Python Sandbox Escape Prevention — Core Concepts

Why Python Is Hard to Sandbox

Python was designed for developer productivity, not isolation. Its core philosophy — “we’re all consenting adults” — means the language provides no meaningful access control boundaries. Every object can inspect and modify other objects. Modules can be imported dynamically. The interpreter itself is accessible from running code.

This isn’t a bug — it’s a fundamental design choice. But it makes sandboxing Python within Python essentially impossible for determined attackers.

How Sandbox Escapes Work

The __builtins__ Pathway

Even if you restrict import, Python objects carry references to the entire runtime. Any object’s class hierarchy eventually reaches object, and from there you can traverse to every loaded module, including os, subprocess, and sys.

The classic technique: start with a string literal, walk up to its class, then to object, enumerate all subclasses, and find one that provides file or process access. This works even when import is completely disabled.

The __globals__ Backdoor

Every function object has a __globals__ attribute containing the module’s global namespace. If you can access any function from a standard library module, you can reach that module’s imports — which often include os, sys, or subprocess.

The code Object Manipulation

Python function objects contain __code__ objects that can be replaced at runtime. An attacker can construct a code object that calls arbitrary C functions through the interpreter’s internal mechanisms.

Approaches That Don’t Work

Restricting import

You can override __import__ or remove modules from sys.modules, but Python provides too many alternative paths: importlib, __builtins__.__import__, dynamic attribute access on existing objects, and ctypes (if available).

Removing Dangerous Functions

Deleting os.system from the namespace doesn’t help when the attacker can reimport os through object traversal. Even if you could remove every dangerous function, Python’s ctypes module provides direct access to C libraries, enabling arbitrary system calls.

RestrictedPython

The RestrictedPython library compiles Python code with restrictions — blocking attribute access to underscore-prefixed names, wrapping function calls, and guarding iteration. It’s useful for very limited DSL scenarios (template rendering, simple expressions) but is not a security boundary for determined attackers. New escape techniques are discovered regularly.

AST Filtering

Parsing code with ast and rejecting dangerous nodes (imports, attribute access to __-prefixed names) catches obvious attacks but misses computed attribute access (getattr), string-based construction, and encoding tricks.

Approaches That Actually Work

OS-Level Isolation

Containers (Docker, Podman): Run untrusted code in a container with minimal privileges — no network, read-only filesystem, restricted system calls via seccomp. The Linux kernel enforces the boundaries, not Python.

Virtual Machines: The strongest isolation. Each execution runs in a separate VM with its own kernel. Services like AWS Lambda and Google Cloud Functions use microVMs (Firecracker) for this purpose.

seccomp-bpf: Linux’s secure computing mode filters system calls at the kernel level. Even if Python code tries to call os.execve(), the kernel blocks the system call before it executes. This is how Chrome’s sandbox works.

nsjail/bubblewrap: Lightweight sandboxing tools that use Linux namespaces, cgroups, and seccomp to create isolated environments without the overhead of full containers.

WebAssembly (WASM)

Compile Python to WebAssembly and run it in a WASM runtime. The runtime provides memory isolation and controlled system call access. Projects like Pyodide (Python in the browser) demonstrate this approach.

Process-Level Isolation with Resource Limits

Run the code in a subprocess with strict resource limits: CPU time (prevent infinite loops), memory (prevent exhaustion), no network access, no filesystem access beyond a temporary directory, and kill the process after a timeout.

Real-World Implementations

Online judges (LeetCode, HackerRank): Use container-per-submission with seccomp profiles, network disabled, and strict time/memory limits.

Jupyter hubs: Run each user’s kernel in a separate container. The notebook server communicates via ZeroMQ, never sharing a process.

Google Colab: Uses full VMs (one per user session) with network restrictions and resource quotas.

Plugin systems: Instead of sandboxing Python directly, define a narrow API and communicate via IPC (JSON-RPC, gRPC). The plugin runs in a separate process with restricted permissions.

Common Misconception

“If I block import os and import subprocess, the code is safe.” This only blocks the most obvious attack. Python’s object model provides dozens of alternative paths to the same functionality. Blocking specific imports is like locking the front door while leaving every window open. True isolation requires OS-level enforcement, not Python-level restrictions.

The one thing to remember: never trust Python to sandbox itself — the language’s power and flexibility are exactly what make it impossible to contain from within; always use external isolation (containers, VMs, seccomp) for untrusted code.

pythonsecurityruntime

See Also