Safeguard
Security Guides

Pickle and Deserialization Security in Python

Unpickling untrusted data is arbitrary code execution, by design. Here is why pickle is dangerous, where it hides, and what to use instead.

Priya Mehta
Security Researcher
5 min read

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 joblib or torch.load often 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.load on a .npy file with allow_pickle=True will 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 haveDo this
Untrusted network payloadJSON / protobuf, never pickle
Config filetomllib or yaml.safe_load
ML model from a hubPrefer safetensors; verify source
Legacy pickle you produceHMAC-sign, verify before load
Numpy .npy from outsideallow_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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.