The Python documentation states it plainly: the pickle module is not secure, and you should never unpickle data from an untrusted or unauthenticated source. This is not a warning about a bug that might get fixed. It is a description of how pickle works. Deserializing a pickle can execute arbitrary Python code, and it does so by design.
Why unpickling is code execution
Pickle is not a data format like JSON. It is a small stack-based virtual machine that reconstructs Python objects, and part of reconstruction is invoking callables. An object can define __reduce__ to specify how it is rebuilt, and a malicious payload uses that hook to call anything it likes the instant you load it:
import pickle, os
class Exploit:
def __reduce__(self):
# Whatever this returns is CALLED during unpickling
return (os.system, ("id",))
payload = pickle.dumps(Exploit())
# The attacker sends `payload`. The victim simply does:
pickle.loads(payload) # os.system("id") runs here
No parsing bug is involved. pickle.loads did exactly what pickle is meant to do. That is why the threat model is binary: if the bytes could have been influenced by an attacker, loading them is remote code execution.
Where pickle hides in real systems
Few developers call pickle.loads on an obvious network payload. The danger is that pickle is buried inside conveniences you may not associate with serialization:
- Machine learning models saved with
joblibortorch.loadoften use pickle under the hood. A model file downloaded from a hub is untrusted input. - Caches and session stores that serialize objects (some configurations of caching layers) may pickle values.
- Task queues that pass Python objects between workers historically defaulted to pickle serializers.
numpy.loadon a.npyfile withallow_pickle=Truewill execute embedded pickles.
The lesson: any file or byte stream that becomes a Python object could be a pickle, and "I downloaded this model" is not the same as "this data is trusted."
The safe defaults
For data interchange, use a format that describes data and only data. JSON is the right default for most application payloads:
import json
data = json.loads(request.body) # cannot execute code, only parses values
For configuration and richer structures, tomllib (standard library in modern Python) and YAML with yaml.safe_load are safe; never use yaml.load without a safe loader, since full YAML can also construct arbitrary objects. For cross-language messaging with a schema, Protocol Buffers or Avro give you both safety and validation.
If you are stuck with pickle
Sometimes you inherit a pickle-based interface you cannot immediately replace. Two mitigations reduce the risk, though neither makes untrusted pickle safe:
First, authenticate the bytes before you load them. If you produced the pickle and stored it somewhere semi-trusted, attach an HMAC with a secret key and verify it before unpickling, so an attacker cannot substitute a payload:
import hmac, hashlib, pickle
def dump_signed(obj, key: bytes) -> bytes:
body = pickle.dumps(obj)
sig = hmac.new(key, body, hashlib.sha256).digest()
return sig + body
def load_signed(blob: bytes, key: bytes):
sig, body = blob[:32], blob[32:]
expected = hmac.new(key, body, hashlib.sha256).digest()
if not hmac.compare_digest(sig, expected):
raise ValueError("Signature mismatch - refusing to unpickle")
return pickle.loads(body)
Second, restrict what a pickle may import by subclassing Unpickler and rejecting everything outside a tight allowlist. This narrows the attack surface but is easy to get wrong; treat it as hardening, not a green light for untrusted input:
import pickle, io
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == "builtins" and name in {"list", "dict", "set"}:
return super().find_class(module, name)
raise pickle.UnpicklingError(f"Blocked: {module}.{name}")
For ML specifically, prefer non-executable formats such as safetensors, which store tensors without any code path, over pickle-backed checkpoints.
Find pickle before an attacker does
You cannot fix what you do not know you are running. Audit your codebase and your dependencies for the entry points, because the risky call is often several layers down. Grep for the obvious ones and treat each hit as a question about data provenance:
grep -rEn "pickle\.loads|pickle\.load|joblib\.load|torch\.load|yaml\.load\(|allow_pickle=True" src/
For each match, ask one question: could the bytes reaching this call have been influenced by anyone outside your trust boundary? If the answer is yes or "not sure," that call needs to change. Pay special attention to task queues and multiprocessing, which historically default to pickle for passing objects between workers, so a compromised queue broker becomes code execution across every worker that reads from it.
Decision table
| You have | Do this |
|---|---|
| Untrusted network payload | JSON / protobuf, never pickle |
| Config file | tomllib or yaml.safe_load |
| ML model from a hub | Prefer safetensors; verify source |
| Legacy pickle you produce | HMAC-sign, verify before load |
Numpy .npy from outside | allow_pickle=False |
How Safeguard helps
Whether your code calls pickle.loads on untrusted bytes is a design question that only code review and static analysis can answer, and we will not overstate what a dependency tool sees here. Where Safeguard is directly useful is the supply side: insecure-deserialization CVEs are regularly disclosed in serialization libraries, ML frameworks, and caching backends, and our software composition analysis surfaces them in your dependency tree with severity and fix guidance. For a flagged issue, Griffin AI helps you judge whether the vulnerable code path is reachable from your application before you spend a maintenance window on it, and you can run that analysis locally with the Safeguard CLI. Teams weighing this approach against other scanners can review our platform comparisons.
Get started
Move untrusted data off pickle first; that removes the root cause. Then let Safeguard track deserialization CVEs in your dependencies. Sign up free at app.safeguard.sh/register and read more at docs.safeguard.sh.