pickle security

2 min read source

pickle security

The gotcha

Unpickling executes arbitrary code. A malicious pickle payload can run any code on your machine. Never unpickle data from untrusted sources.

Minimal repro

A crafted pickle payload that runs os.system("rm -rf /"):

import pickle, os

class Evil:
    def __reduce__(self):
        return (os.system, ("echo pwned",))

payload = pickle.dumps(Evil())

# Receiver:
pickle.loads(payload)   # prints: pwned

__reduce__ tells pickle how to reconstruct the object. The pickle protocol allows specifying any callable and any arguments. On loads, it calls them. Arbitrary code execution by design.

Why it happens

Pickle isn’t a data format — it’s a serialization of Python’s object construction protocol. To rebuild an object you need to call its constructor (or some factory). Pickle stores enough info to do that, including the callable name. There’s no way to safely “evaluate” a pickle without running code.

This applies to pickle, cPickle, dill, joblib (which uses pickle), and marshal (less direct but similar exposure).

What’s safe?

Source of pickle data Safe to load?
Files you wrote yourself, kept local yes
Files in your own private storage yes
ML model checkpoints from your team’s pipeline if origin verified
Files downloaded from the internet (e.g. HuggingFace) no
User uploads no
Database fields populated by users no
Inter-service messages from external services no

How to mitigate

Best: don’t use pickle for data exchange. Use:

  • JSON — for plain data, no code execution
  • MessagePack / Protobuf / Arrow — for binary, schema-defined
  • YAML with safe_loadyaml.safe_load doesn’t execute tags

If you must use pickle, sign or HMAC your data:

import pickle, hmac, hashlib

KEY = b"secret"

def safe_dumps(obj):
    body = pickle.dumps(obj)
    sig = hmac.new(KEY, body, hashlib.sha256).digest()
    return sig + body

def safe_loads(blob):
    sig, body = blob[:32], blob[32:]
    expected = hmac.new(KEY, body, hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError("tampered")
    return pickle.loads(body)

This blocks tampering but doesn’t help if the key leaks or you load anything from outside.

You can also subclass pickle.Unpickler and override find_class to whitelist allowed types, but this is hard to get right.

ML-specific guidance

PyTorch .pth files use pickle. HuggingFace model files now use safetensors by default for this reason. If you must load a .pth from outside, use torch.load(..., weights_only=True) (PyTorch 1.13+) which uses a restricted unpickler.

Interview angle

“Is pickle safe?” If they answer “yes, it’s just serialization” — wrong. Follow-up: “Show me how an attacker could exploit pickle.loads of user-uploaded data.” A senior candidate writes the __reduce__ payload from memory.