Python exec Sandboxing: Risks and Limitations
Attempting to run untrusted user code safely using Python's built-in
exec() function is a notoriously difficult and generally
flawed approach to security. While developers often try to restrict
execution environments by sanitizing namespaces or stripping
__builtins__, Python’s deeply dynamic architecture makes it
virtually impossible to construct a secure in-process sandbox. This
article examines the core security vulnerabilities, escape mechanisms,
and operational constraints that make exec() unsuitable for
executing untrusted user input.
Introspection and Namespace Escapes
The primary security vulnerability of exec() stems from
Python’s dynamic introspection features. Even if you invoke
exec(code, {"__builtins__": {}}) to remove standard
built-in functions like open(), eval(), or
__import__, a user can traverse the object hierarchy to
reconstruct dangerous references.
Every object inherits from object, and through
attributes such as __class__, __bases__, and
__subclasses__(), an attacker can access loaded modules and
arbitrary system calls. For example, the following expression
reconstructs access to system commands without using explicit
imports:
[c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == 'BuiltinImporter'][0]().load_module('os').system('whoami')Because Python objects, types, and functions carry metadata pointing to their modules and execution contexts, clearing or customizing globals alone does not sever an attacker's access to the runtime environment.
Bytecode Exploitation and CPython Crashes
Relying on code-level sanitization—such as parsing Abstract Syntax
Trees (AST) to block specific attribute access like
__subclasses__—is equally fragile. Attackers can obfuscate
payloads using string manipulation (e.g.,
getattr(obj, "__sub" + "classes__")) or dynamically
construct bytecode.
Furthermore, CPython was not engineered to defend against malicious bytecode. Attackers can deliberately trigger segmentation faults, buffer overflows, or corrupt internal interpreter structures by crafting deeply nested structures or leveraging obscure interpreter behaviors. Once memory corruption occurs within the host process, sandbox boundaries are rendered irrelevant.
Resource Exhaustion and Denial of Service
Beyond privilege escalation and unauthorized file access,
exec() offers zero native operational boundaries for
resource management. Code executed via exec() runs directly
inside the host Python process and thread:
- CPU Saturation: A simple
while True: passconstruct blocks the execution thread indefinitely, leading to CPU starvation. In a standard web application, this easily triggers a denial-of-service (DoS) condition. - Memory Depletion: Code can allocate enormous data
structures (such as
[0] * 10**10), forcing the host operating system to invoke the Out-Of-Memory (OOM) killer, which may terminate the entire host application. - Stack Overflow and Recursion: Manipulating recursion limits or deeply nesting structures can crash the runtime stack without yielding execution back to the host program.
Thread and Signal Non-Isolation
Code executed in exec() shares the exact process space
as the hosting system. This means untrusted code can:
- Mutate shared global state, monkey-patch standard library modules, or corrupt data used by other threads.
- Spawn unbounded native threads via the
threadingor_threadmodules (if reachable), degrading performance. - Trap, register, or silence operating system signals (e.g.,
SIGINT,SIGTERM), preventing the hosting orchestrator from cleanly shutting down or restarting unresponsive processes.
Viable Alternatives
Because in-process isolation inside CPython cannot be guaranteed, executing untrusted code requires external, defense-in-depth isolation strategies:
- MicroVMs: Tools like AWS Firecracker provide hardware-level virtualization with minimal startup latency.
- Sandboxed Containers: Technologies such as Google’s gVisor or standard OCI containers running as unprivileged users with strict cgroups, limited capabilities, and read-only filesystems.
- WebAssembly (Wasm): Compiling a Python interpreter (such as Pyodide or MicroPython) to Wasm, executing it inside a restricted runtime that strictly limits memory and system API exposure.