A Python pickle exploit is a code-execution attack that works because the pickle module can run arbitrary code while deserializing data — so anyone who can feed a malicious pickle to pickle.loads() can execute code in your process. This is not a bug in a particular version that a patch will fix. It is documented behavior: the Python docs explicitly warn that the pickle module is not secure and that you must never unpickle data from an untrusted or unauthenticated source. Understanding the mechanism defensively is the only way to stop reaching for pickle in the wrong places.
We will look at why this happens by design, how to recognize the risk in a codebase, and what to use instead — without publishing a working exploit against any real target.
Why unpickling executes code at all
Serialization formats fall into two camps. Data-only formats (JSON, CSV, Protocol Buffers) describe values — strings, numbers, lists — and a parser reconstructs those values. Object-serialization formats like pickle go further: they encode how to rebuild an object, which can include instructions to call functions and construct classes.
The pickle format is essentially a small stack-based program. When you unpickle, the interpreter executes that program's opcodes to rebuild the object graph. One of those opcodes can invoke a callable. Python objects can also define a __reduce__ method that tells pickle exactly how to reconstruct them — and __reduce__ can return a callable plus arguments that pickle will then invoke during loading.
That is the whole vulnerability class. An attacker crafts an object whose reconstruction instructions call something dangerous, serializes it, and gets you to unpickle it. The execution happens inside pickle.loads(), before your code ever inspects the "data." Conceptually:
# ILLUSTRATIVE ONLY — shows the mechanism, not a usable payload
import pickle
class Demo:
def __reduce__(self):
# A real exploit puts a dangerous callable here.
# __reduce__ returns (callable, args) that pickle will invoke on load.
return (print, ("this ran during unpickling",))
blob = pickle.dumps(Demo())
pickle.loads(blob) # "this ran during unpickling" prints here
The print above is harmless on purpose. The point is the timing: something ran during the load, controlled entirely by whoever produced the blob. Swap in any callable and the severity follows.
Where the risk hides in real systems
The danger is not people typing pickle.loads on obviously hostile input. It is the places pickle sneaks in indirectly:
- Caches and session stores. A cache backend that pickles values means anyone who can write to the cache (a compromised Redis, a cache-poisoning bug) can achieve code execution when the value is read back.
- Message queues and task frameworks. Some job systems historically defaulted to pickle for serializing task arguments. A queue an attacker can write to becomes an RCE channel.
- Model files. Machine-learning model formats built on pickle (including some
.pt/.pklcheckpoints and joblib artifacts) execute code on load. Downloading a model from an untrusted source and loading it is a python exploit waiting to happen — this has driven real-world advisories in the ML ecosystem. pandas.read_pickle,numpy.load(allow_pickle=True),torch.load. These are pickle under the hood and carry the same risk.
If any of these read data that crosses a trust boundary, you have exposure.
Detection
You cannot sanitize a malicious pickle after the fact — by the time you have a way to inspect it, loading it has already run the code. Detection therefore focuses on finding the dangerous call sites before they run:
-
Static analysis.
banditflagspickle,cPickle, and related loads asB301/B403. Grep works as a blunt first pass:grep -rn "pickle.load\|pickle.loads\|read_pickle\|allow_pickle=True\|torch.load" --include=*.py . -
Trace the data source for each hit. The question for every call site is: can the bytes being unpickled ever originate from outside your trust boundary? If yes, it is a finding.
-
Audit dependencies. Libraries you pull in may unpickle internally. An SCA tool helps you see which dependencies touch serialization and whether any carry advisories for unsafe deserialization defaults.
The fix: change the format, do not "harden" pickle
There is no safe way to unpickle untrusted data. Attempts to build a "restricted unpickler" that blocks dangerous globals help but are notoriously easy to bypass and should not be your primary defense. The durable fixes:
- Use a data-only format for anything crossing a trust boundary. JSON for simple structures; MessagePack or Protocol Buffers when you need efficiency and a schema. None of these can execute code on load.
- If you must transport rich Python objects, sign them. Compute an HMAC over the serialized bytes with a secret key and verify it before loading, so you only unpickle data your own system produced. This does not make pickle safe against a leaked key — it establishes provenance.
- For ML models, prefer safetensors or another format designed to be load-safe, and treat any pickle-based checkpoint from an external source as untrusted code.
- Isolate unavoidable pickle loads in a sandboxed, least-privilege process if you truly cannot change the format.
# Safe by construction: JSON cannot execute code on load
import json
data = json.loads(payload) # payload can be hostile; this only parses values
Our security academy covers the broader deserialization class — this same pattern (data-plus-behavior formats executing on load) recurs in Java, .NET, and Ruby, and the defense is always the same: keep untrusted input in a format that only describes data.
FAQ
Is pickle always dangerous?
Pickle is safe when both the producer and consumer are inside your trust boundary — for example, caching data your own code created and no attacker can tamper with. It becomes a code-execution risk the moment the bytes being unpickled could originate from an untrusted or attacker-influenced source.
Can I make pickle.loads safe with a restricted unpickler?
Restricting which globals an unpickler allows raises the bar but is not a reliable defense — bypasses are common and the approach is easy to get subtly wrong. For untrusted input, switch to a data-only format like JSON or MessagePack instead of trying to harden pickle.
Why is loading a machine-learning model risky?
Many model formats are built on pickle, so loading a model file executes the reconstruction instructions embedded in it, including any callables an attacker placed there. Loading a model from an untrusted source is equivalent to running untrusted code. Prefer load-safe formats such as safetensors, and treat external checkpoints as untrusted.
What should I use instead of pickle?
For data crossing a trust boundary, use JSON for simple structures or MessagePack / Protocol Buffers for performance and schemas — none can execute code on load. Reserve pickle for internal, tamper-proof paths, and if you must move rich objects across a boundary, sign the bytes with an HMAC and verify before loading.