CPython security model: what is and is not sandboxed, restricted execution limits, and audit hook coverage.
CPython is a language runtime, not a sandbox. It provides mechanisms for executing Python code, loading modules, managing objects, calling native extensions, opening files, creating processes, using sockets, and interacting with the operating system. These features are useful because Python is a general-purpose programming language, but they also mean that executing untrusted Python code inside ordinary CPython is unsafe.
A security boundary is a line across which one side should not be able to violate the integrity, confidentiality, or availability of the other side.
Examples:
user process vs operating system
web request vs server runtime
plugin vs host application
serialized data vs application state
Python code vs native extension
subinterpreter vs processCPython has many isolation mechanisms, but most are not complete security boundaries by themselves.
This chapter examines:
why CPython is not a sandbox
what counts as a real security boundary
where Python code can escape restrictions
how imports affect security
why eval and exec are dangerous
why pickle is unsafe for untrusted input
how native extensions change the threat model
how subprocesses and OS isolation help
what subinterpreters can and cannot isolate99.1 CPython Trust Model
Ordinary CPython assumes that code running in the interpreter is trusted.
A Python program can normally:
read and write files
import modules
open network sockets
start subprocesses
inspect objects
mutate globals
use reflection
load native extensions
consume memory
consume CPUExample:
import os
os.remove("data.txt")This is ordinary Python behavior, not an exploit.
Therefore, the first rule is:
Do not execute untrusted Python code in a normal CPython interpreter and expect containment.99.2 Language Restrictions Are Not Enough
A common mistake is to remove a few builtins and assume code is safe.
Example:
safe_globals = {
"__builtins__": {}
}
eval(user_input, safe_globals, {})This looks restricted, but Python’s object model is highly reflective.
Objects can expose paths back to powerful runtime capabilities through:
classes
subclasses
frames
tracebacks
function globals
descriptors
import machinery
object introspectionTrying to build a secure sandbox by filtering names is fragile.
The problem is that Python objects are not passive data. They carry behavior, type relationships, metadata, and access paths into the runtime.
99.3 eval and exec
eval evaluates an expression.
eval("1 + 2")exec executes statements.
exec("x = 1\nprint(x)")Both execute Python code.
If input comes from an attacker, these functions cross a security boundary.
Bad:
expr = request.args["expr"]
result = eval(expr)An attacker can run arbitrary Python code.
Even restricted globals are often insufficient.
Safer alternatives depend on the use case:
parse numbers manually
use ast.literal_eval for Python literals
use a real expression parser
use a small domain-specific language
evaluate in a separate OS sandboxast.literal_eval only accepts Python literal structures such as strings, numbers, tuples, lists, dicts, sets, booleans, and None.
import ast
value = ast.literal_eval("[1, 2, 3]")This is much safer than eval, but it still needs resource limits for hostile inputs.
99.4 Import Is Code Execution
Importing a module executes top-level code.
import pluginIf plugin.py contains:
import os
os.system("rm -rf /tmp/example")then importing it runs that code.
This matters for plugin systems.
A plugin import is not merely metadata loading. It gives the plugin execution inside the host process.
A secure plugin system cannot rely on import alone for isolation. It needs a real containment layer, usually at the process or operating system level.
99.5 Module Search Path Attacks
The import system searches paths in order.
If an attacker controls a directory on sys.path, they may shadow trusted modules.
Example:
project/
json.pyThen:
import jsonmay import the local file instead of the standard library module.
Security-sensitive programs should control:
current working directory
PYTHONPATH
sys.path mutation
.pth files
site initialization
virtual environment contents
plugin directoriesImport path control is part of the security model.
99.6 Pickle Is Code Execution
pickle can reconstruct arbitrary Python objects.
During unpickling, it may import modules and call functions.
Therefore:
pickle.loads(untrusted_bytes) is unsafeBad:
import pickle
obj = pickle.loads(request.body)An attacker can craft a pickle payload that executes code during loading.
For untrusted data, use data formats that do not encode arbitrary Python object reconstruction:
JSON
Protocol Buffers
MessagePack with validation
CBOR with validation
custom schema formatsEven then, input size and structure must be limited.
99.7 Marshal Is Also Unsafe
marshal is a CPython internal serialization format.
It should not be used for untrusted input.
Bad:
import marshal
code = marshal.loads(data)
exec(code)This can load code objects and execute them.
marshal is used for CPython internals such as .pyc files. It is not a secure data interchange format.
99.8 JSON Is Safer but Not Complete Security
JSON represents data, not executable Python objects.
import json
data = json.loads('{"name": "Ada"}')This avoids the main code execution problem of pickle.
However, JSON parsing can still be abused through:
very large inputs
deep nesting
large strings
large arrays
schema confusion
memory exhaustion
CPU exhaustionSecurity still requires:
input size limits
schema validation
timeouts
resource quotas
clear type checksData-only formats reduce risk. They do not remove all risk.
99.9 Native Extensions Break Python-Level Isolation
CPython extension modules run native code inside the Python process.
A C extension can:
read and write arbitrary process memory if buggy
crash the interpreter
corrupt Python objects
bypass Python-level checks
call operating system APIs directly
release the GIL
start native threadsThis means Python-level restrictions do not contain native extensions.
If untrusted code can import arbitrary extension modules, the process cannot be considered safe.
Native code changes the threat model from language-level safety to process-level memory safety.
99.10 ctypes and FFI
ctypes lets Python call native shared libraries.
Example:
import ctypes
libc = ctypes.CDLL(None)With FFI access, code can call low-level system functions.
This is powerful and useful, but it defeats most language-level sandboxing.
A restricted execution environment must prevent access to:
ctypes
cffi
native extension loading
dlopen mechanisms
subprocess access
filesystem paths containing native librariesOtherwise, Python code can escape high-level restrictions.
99.11 Reflection and Introspection
Python exposes extensive introspection.
Code can inspect:
object types
class hierarchies
function globals
closures
frames
tracebacks
modules
loaded subclassesExample:
def f():
pass
print(f.__globals__)The function’s globals dictionary may expose modules and powerful objects.
Frame inspection is also sensitive:
import inspect
frame = inspect.currentframe()
print(frame.f_globals)If a sandbox leaves access to frames, tracebacks, or function globals, restricted code may recover capabilities that were intentionally removed.
99.12 Attribute Access Is Programmable
Attribute access can execute code.
Example:
class User:
@property
def name(self):
print("running code")
return "Ada"
u = User()
u.nameThis means even reading an attribute can trigger arbitrary behavior.
Mechanisms involved include:
__getattribute__
__getattr__
descriptors
properties
metaclasses
module __getattr__Security code should avoid treating arbitrary Python objects as inert records.
For untrusted data, prefer plain validated data structures.
99.13 Operators Can Execute Code
Operators are dispatch hooks.
Example:
class Evil:
def __add__(self, other):
print("running code")
return 0
Evil() + 1The same applies to:
comparison
hashing
iteration
containment checks
string conversion
formatting
boolean tests
context managers
awaitingEven innocent-looking operations may call user-defined methods.
Security-sensitive code should be careful when operating on attacker-controlled objects.
99.14 Denial of Service
Security is not only about code execution.
Untrusted input can attack availability.
Examples:
"9" * 10_000_000[0] * 1_000_000_000while True:
passPotential denial-of-service vectors:
CPU exhaustion
memory exhaustion
file descriptor exhaustion
thread explosion
process explosion
deep recursion
pathological regex behavior
huge decompression outputs
large integer arithmeticA safe execution environment needs resource limits, not just permission checks.
99.15 Regular Expression Hazards
Some regular expressions have catastrophic backtracking behavior.
Example pattern shape:
(a+)+$On certain inputs, matching time can grow exponentially.
If regex patterns or inputs are attacker-controlled, use:
timeouts
safe regex engines
bounded input sizes
careful pattern design
prevalidationThis is a common availability boundary issue.
99.16 Filesystem Boundaries
Python file APIs expose direct filesystem access.
Examples:
open("/etc/passwd").read()from pathlib import Path
Path("data.txt").unlink()Filesystem security depends on the operating system:
user permissions
chroot or containers
mount namespaces
read-only filesystems
path validation
directory permissions
temporary file safetyApplication-level path filtering is easy to get wrong.
Example path traversal:
../../../../etc/passwdAlways normalize and constrain paths against an allowed root.
99.17 Subprocess Boundaries
Python can create subprocesses.
import subprocess
subprocess.run(["ls", "-la"])When building commands from user input, avoid shell interpolation.
Bad:
subprocess.run("grep " + user_input + " file.txt", shell=True)Safer:
subprocess.run(["grep", user_input, "file.txt"], shell=False)But even shell-free subprocess calls require careful control over:
executable path
arguments
environment variables
working directory
file descriptors
timeouts
resource limits99.18 Environment Variables
Environment variables affect Python behavior and native libraries.
Examples:
PYTHONPATH
PYTHONHOME
PYTHONSTARTUP
PYTHONWARNINGS
LD_LIBRARY_PATH
DYLD_LIBRARY_PATH
SSL_CERT_FILE
PATHA privileged or security-sensitive Python program should sanitize its environment.
Otherwise, attackers may influence imports, native library loading, subprocess behavior, or TLS configuration.
99.19 Audit Hooks
CPython includes audit hooks that can observe certain security-relevant runtime events.
Examples of event categories:
imports
file opens
subprocess creation
socket operations
code execution
dynamic loadingAudit hooks are useful for logging, monitoring, and policy enforcement.
However, they are not a full sandbox by themselves.
They run inside the same process. Native code or already-compromised runtime state can bypass or disable assumptions depending on deployment.
Audit hooks are best treated as detection and control aids, not complete isolation.
99.20 Subinterpreters Are Not Security Sandboxes
Subinterpreters provide runtime isolation between interpreter states.
They can separate:
module dictionaries
builtins
globals
thread states
some import state
some GC stateBut subinterpreters live in the same process.
They share:
address space
native libraries
process credentials
file descriptors
operating system resources
some runtime stateA crash or memory corruption in one subinterpreter can affect the whole process.
Therefore:
subinterpreters are an isolation mechanism, not a strong security boundary.They are useful for concurrency and organization, but not sufficient for hostile code.
99.21 Threads Are Not Security Boundaries
Threads share memory.
A Python thread can access process-global state, imported modules, and shared objects.
Even with the GIL or per-interpreter GIL, threads do not provide protection from malicious code.
Threads are concurrency mechanisms, not containment mechanisms.
99.22 Processes Are Stronger Boundaries
Operating system processes provide stronger isolation.
Separate processes can have:
separate address spaces
separate resource limits
separate user IDs
separate filesystem views
separate namespaces
separate seccomp filters
separate container policiesFor untrusted Python code, prefer process-level isolation.
A common model:
host process
validates request
starts or reuses sandbox worker process
sends limited input
enforces timeout and memory limit
receives result
kills worker on violationThis gives the host a real boundary outside the Python object model.
99.23 Containers and OS Sandboxing
Containers can help isolate Python execution.
Useful mechanisms include:
Linux namespaces
cgroups
seccomp
AppArmor
SELinux
read-only filesystems
dropped capabilities
network isolation
temporary working directories
non-root usersA container alone is configuration, not a guarantee.
A secure container must be deliberately constrained.
Bad:
root user
host filesystem mounted
docker socket mounted
privileged mode
unlimited CPU and memory
host networkGood:
non-root user
read-only root filesystem
small writable temp directory
no host mounts
no docker socket
limited memory
limited CPU
no unnecessary network
dropped capabilities
seccomp profile99.24 Web Server Boundaries
In a Python web server, request handlers run inside the same interpreter process unless isolated explicitly.
A bug in one request handler can affect:
global module state
connection pools
caches
environment
process memory
other requestsSecurity-sensitive web applications should separate concerns:
validate input at boundaries
avoid global mutable request state
treat deserialization carefully
avoid eval and exec
limit upload sizes
use process isolation for untrusted execution
apply least privilege to deployment99.25 Plugin Systems
Plugins are difficult to secure.
Importing a plugin gives it execution in the host process.
A trusted plugin model may be acceptable:
plugins are installed by administrators
plugins run with host privileges
plugins can call host APIs directlyAn untrusted plugin model needs stronger boundaries:
separate process
restricted filesystem
message passing API
resource limits
signed packages
capability-based permissions
network restrictionsDo not treat Python package import as a permission system.
99.26 Supply Chain Boundaries
Installing a Python package runs or exposes code from another party.
Risks include:
malicious packages
typosquatting
compromised maintainers
dependency confusion
build-time code execution
native extension payloads
post-install behaviorSecurity controls include:
pin dependencies
use lock files
verify hashes
review transitive dependencies
use private indexes carefully
isolate builds
avoid running package setup code with broad privileges
scan wheels and sdists
prefer reproducible builds where possibleThe package boundary is a trust boundary.
99.27 Embedding CPython
Applications can embed CPython as a scripting engine.
Embedding does not automatically make Python scripts safe.
The host must decide:
what code may run
what modules may import
what filesystem paths are visible
what native extensions are allowed
what resources are limited
how script failures are containedFor trusted scripting, embedding is straightforward.
For untrusted scripting, use process isolation or a dedicated sandbox runtime.
99.28 Practical Rules
Use these rules:
Do not execute untrusted Python code in ordinary CPython.
Do not rely on removing builtins as a sandbox.
Do not unpickle untrusted data.
Do not import untrusted plugins in the host process.
Do not allow arbitrary native extensions in restricted environments.
Use OS process isolation for hostile code.
Use resource limits for CPU, memory, file descriptors, and time.
Use schema validation for external data.
Control sys.path and environment variables in privileged programs.
Treat package installation as a trust decision.99.29 Mental Model
Use this model:
CPython gives Python code broad access to the runtime and operating system.
Python-level restrictions are weak because:
objects are reflective
imports execute code
operators call user methods
functions expose globals
frames expose runtime state
native extensions bypass language controls
Strong boundaries come from outside the interpreter:
OS processes
users and permissions
containers
resource limits
seccomp and sandbox policies
message passing
Subinterpreters, threads, globals, and restricted dictionaries are useful mechanisms, but they are not complete security boundaries.99.30 Chapter Summary
CPython is not a secure sandbox for untrusted code.
The main hazards are:
eval and exec
unsafe deserialization
import-time code execution
module path manipulation
native extensions
ctypes and FFI
reflection and frames
resource exhaustion
subprocess and filesystem access
supply chain attacksReal isolation requires process-level or operating-system-level boundaries.
Within CPython, security engineering means treating code execution, imports, serialization, native extensions, and package installation as explicit trust decisions.