Python Vulnerability Anatomy
Python’s greatest strengths as a language are also its broadest attack surface. It is dynamic, interpreted, and deeply introspective: code can build and run more code at runtime, objects can be serialized into byte streams that reconstruct arbitrary state, the import machinery can be bent to run code before a program’s first line, and every object exposes a path to every other object through its type and method-resolution graph. None of these are bugs. They are documented, intended behavior, and that is exactly why they are dangerous, an attacker abusing them is operating the language as designed, not corrupting it.
This section dissects Python vulnerability classes at the language level, the mechanics of why a technique works, independent of any framework or application. These are the primitives that turn “attacker controls a string” into “attacker runs code”: command and dynamic-code-execution sinks, insecure deserialization, path traversal, object-graph traversal, import-system abuse, and sandbox escapes. Learn the anatomy once and you recognize the same bug wherever Python runs, a web route, a CLI tool, a CI pipeline, a data-science notebook, code that never touches the web at all.
Two lenses run through every page: the sink (the dangerous call that turns data into action, os.system, pickle.loads, eval, open) and the chain (how an attacker reaches that sink and escalates from “expression evaluates” to “code executes”). Offensively, you hunt for the sink and then build the chain; defensively, you cut the data path before it reaches the sink.
What this section covers
Injection & code execution
- Python Command Injection: every shell-invoking sink (
os.system,os.popen,subprocess.*,shell=True) and how user input breaks out of the intended command - Insecure Dynamic Code Evaluation and Execution in Python:
eval()andexec()turning strings into running code
Deserialization & object injection
- Serialization and Deserialization Concept: a primer on why richer formats are more dangerous
- Insecure Deserialization - Python Pickle:
__reduce__and arbitrary code execution onpickle.loads - Insecure Deserialization - Unsafe YAML Loading:
yaml.load/UnsafeLoaderobject construction - Insecure Deserialization - Python Shelve: pickle RCE hiding behind a dict-like API
File access
- Insecure File Access and Path Traversal in Python:
open,os.path.join,pathlib, and why existence checks are not a defense
Python internals as attack surface
- Walking the Python Object Graph with subclasses(): the
__class__→__mro__→__subclasses__()→__globals__chain that powers most jail and sandbox escapes - Import System Abuse with .pth Files and sys.meta_path: hijacking import resolution for code execution and persistence
- Escaping Python exec and eval Sandboxes: breaking out of restricted execution environments and
__builtins__-stripped jails
Every technique here has a runnable counterpart under generic-py-fu/ in the PyFu lab (these are standalone scripts, not web apps). The framework-specific exploitation of the same classes, the Flask SSTI/SQLi/XXE apps, the FastAPI auth bypasses, lives in the Web Application Attacks section and is wired into the Docker lab.
Why Python’s native attack surface matters from an offensive security perspective
I keep this section as the first stop on every Python assessment because the bugs documented here are framework-independent and they pay in full. The same dynamic features that make Python pleasant to write, runtime evaluation, rich serialization, a programmable import system, and an object graph that exposes everything to everything, are exactly the primitives that turn “attacker controls a string” into “attacker runs code”. When I find one of these sinks, I am usually not looking at information disclosure or a privilege nudge; I am looking at RCE, persistence, or a sandbox escape. That is why I hunt the sink first and worry about the framework second.
What makes this surface valuable to me is that it is native and quiet. There is no exploit binary, no memory corruption, no version-specific gadget that breaks on the next patch. The techniques abuse documented, intended language behavior, so they survive upgrades and rarely trip the controls teams actually deploy. A __subclasses__() climb works the same in a template engine, a pickle gadget, and a homemade eval jail, which means one mental model unlocks several bug classes at once.
When I audit a Python codebase, these are the tells I grep for first:
- Any sink that turns data into action.
eval,exec,compile,os.system,subprocess(..., shell=True),pickle.loads,yaml.load,__import__. Each one is a direct line from input to execution, and I trace the data path backward to the nearest attacker source. - A “sandbox” or “safe eval” built inside the interpreter. Stripped
__builtins__, name blocklists, and restricted namespaces signal a developer who knew the feature was dangerous and tried to contain it in the wrong layer. That is an escape waiting to happen, not a defense. - Deserialization of anything the user can influence. Session blobs, cache entries, message-queue payloads, and “config” files that round-trip objects are all
__reduce__territory. - Writable import locations. A
.pthfile or asys.meta_pathinsertion means code runs before the program’s first line or hijacks a future import, with nothing visible in the application’s source. - Path values that reach
open,os.path.join, orpathlibwithout canonicalization. Existence checks and prefix checks are not containment.
For defenders the takeaway is blunt: Python’s expressiveness is the attack surface, so the only durable control is to keep untrusted data away from these sinks entirely rather than to filter what reaches them, because the object model offers too many equivalent paths to the same capability.