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:

Thread and Signal Non-Isolation

Code executed in exec() shares the exact process space as the hosting system. This means untrusted code can:

Viable Alternatives

Because in-process isolation inside CPython cannot be guaranteed, executing untrusted code requires external, defense-in-depth isolation strategies: