Skip to content

99. Security Boundaries

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 process

CPython 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 isolate

99.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 CPU

Example:

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 introspection

Trying 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 sandbox

ast.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 plugin

If 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.py

Then:

import json

may 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 directories

Import 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 unsafe

Bad:

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 formats

Even 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 exhaustion

Security still requires:

input size limits
schema validation
timeouts
resource quotas
clear type checks

Data-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 threads

This 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 libraries

Otherwise, 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 subclasses

Example:

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.name

This 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() + 1

The same applies to:

comparison
hashing
iteration
containment checks
string conversion
formatting
boolean tests
context managers
awaiting

Even 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_000
while True:
    pass

Potential 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 arithmetic

A 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
prevalidation

This 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 safety

Application-level path filtering is easy to get wrong.

Example path traversal:

../../../../etc/passwd

Always 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 limits

99.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
PATH

A 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 loading

Audit 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 state

But subinterpreters live in the same process.

They share:

address space
native libraries
process credentials
file descriptors
operating system resources
some runtime state

A 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 policies

For 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 violation

This 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 users

A 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 network

Good:

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 profile

99.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 requests

Security-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 deployment

99.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 directly

An untrusted plugin model needs stronger boundaries:

separate process
restricted filesystem
message passing API
resource limits
signed packages
capability-based permissions
network restrictions

Do 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 behavior

Security 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 possible

The 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 contained

For 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 attacks

Real 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.