Safeguard
DevSecOps

Pickling in Python: A Security Guide

Pickling in Python serializes objects to bytes, but unpickling untrusted data can run arbitrary code. Here is how the risk works and how to defend against it.

Priya Mehta
Security Analyst
6 min read

Pickling in Python is the built-in way to serialize almost any object into a byte stream, but unpickling data you did not create yourself can execute arbitrary code on your machine. That single property is why the pickle module carries a warning at the top of its own documentation and why it shows up so often in security reviews of Python services.

This guide explains what pickling in Python actually does, walks through why the danger is real, and gives you concrete patterns for handling serialized data without opening a remote code execution hole.

What Is Pickling in Python?

Pickling is Python's native object serialization. The pickle module converts a live object graph into a compact binary representation you can write to disk or send over a socket, and unpickling reverses the process to reconstruct the object.

import pickle

data = {"user": "priya", "roles": ["admin", "auditor"]}

# Pickling: object -> bytes
blob = pickle.dumps(data)

# Unpickling: bytes -> object
restored = pickle.loads(blob)

The appeal is that pickling and unpickling in Python handle types JSON cannot: custom classes, sets, datetimes, NumPy arrays, and nested references all round-trip without extra code. Frameworks lean on it heavily. Celery task payloads, some caching layers, multiprocessing, and older machine-learning model files all use pickle under the hood.

That convenience is exactly what makes it risky.

Why Unpickling Untrusted Data Is Dangerous

When you ask "what is pickling python doing internally," the honest answer is that it runs a small stack-based virtual machine. The pickle format includes opcodes that can import modules and call callables during reconstruction. An attacker who controls the byte stream can craft a payload that runs whatever they want.

The mechanism is the __reduce__ method. When an object is pickled, __reduce__ can return a callable and its arguments that will be invoked at load time.

import pickle
import os

class Exploit:
    def __reduce__(self):
        # Illustrative only: this runs when the object is unpickled
        return (os.system, ("id",))

payload = pickle.dumps(Exploit())
# pickle.loads(payload) would execute the command on the victim's host

This is not a bug in a specific version to be patched away. It is the documented behavior of the format, categorized under CWE-502 (Deserialization of Untrusted Data). The lesson is blunt: never call pickle.loads on bytes that came from a user, a network peer you do not fully trust, or a file whose integrity you cannot guarantee.

Where Pickle Risk Hides in Real Systems

Python pickling rarely appears as an obvious pickle.loads(request.body) call. It hides behind abstractions:

  • Message queues configured with the pickle serializer instead of JSON.
  • Caches that transparently pickle and unpickle values, then get exposed to attacker-controllable keys.
  • Session backends that store pickled objects in cookies or Redis without signing them.
  • Pre-trained model files (.pkl, some .pt and .joblib artifacts) downloaded from public hubs.

That last one matters for anyone doing machine learning. A model file is executable content when it is a pickle. Downloading a .pkl from an untrusted source and loading it is functionally the same as running a stranger's script. An SCA tool such as Safeguard can flag pickle-backed dependencies and model artifacts in your supply chain, but you still need a policy for how those files are trusted.

Safer Alternatives to Pickling

The strongest defense is to not use pickle for data that crosses a trust boundary. Reach for a data-only format instead.

Use JSON for interchange. If your objects are dictionaries, lists, strings, and numbers, json is safe by construction because it has no mechanism to invoke code.

import json

blob = json.dumps({"user": "priya", "roles": ["admin"]})
restored = json.loads(blob)  # cannot execute arbitrary callables

Use a schema-based serializer like Protocol Buffers, MessagePack with a strict schema, or pydantic for validated structured data. These give you the ergonomics of typed objects without the execution semantics.

For NumPy and scientific data, prefer formats designed for arrays such as .npy/.npz with allow_pickle=False, or Parquet, rather than pickling raw objects.

When You Genuinely Must Use Pickle

Sometimes pickle is unavoidable, for example inside a single trusted process boundary like multiprocessing. When that is the case, add layers of defense.

Sign your data. Attach an HMAC computed with a secret key and verify it before unpickling. If the signature does not match, the payload never reaches pickle.loads.

import hmac
import hashlib
import pickle

SECRET = b"rotate-this-key"

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

def load_signed(blob):
    sig, body = blob[:32], blob[32:]
    expected = hmac.new(SECRET, body, hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError("signature mismatch, refusing to unpickle")
    return pickle.loads(body)

Restrict what can be unpickled. You can subclass pickle.Unpickler and override find_class to allow only a specific allowlist of safe classes, rejecting everything else. This does not make pickle safe for arbitrary attacker input, but it narrows the blast radius for semi-trusted data.

Isolate the process. Run any unavoidable unpickling in a sandbox with a restricted user, no network, and a tight seccomp profile so that even a successful payload has little to work with.

Auditing an Existing Codebase

Finding pickle usage is straightforward with a repository-wide search. Look for pickle.load, pickle.loads, cPickle, dill, joblib.load, torch.load without weights_only=True, and message-queue serializer settings. Treat each hit as a question: where does this byte stream come from, and could an attacker influence it? Fold that check into code review and CI so new pickle calls do not slip in unnoticed. If you want a broader primer on catching risky dependency behavior, the Safeguard Academy covers deserialization and other injection classes.

FAQ

Is pickling in Python always insecure?

No. Pickling itself is safe when you control both ends and the data never crosses a trust boundary. The danger is unpickling bytes that an attacker could have tampered with, which can lead to code execution.

What is the difference between pickling and unpickling in Python?

Pickling converts a Python object into a byte stream with pickle.dumps or pickle.dump. Unpickling reverses it with pickle.loads or pickle.load to rebuild the object. The security risk lives almost entirely on the unpickling side.

Is JSON a safe replacement for python pickling?

For plain data structures, yes. JSON has no facility to import modules or call functions during parsing, so it cannot be coerced into running code the way pickle can. The trade-off is that JSON only handles basic types.

How do I load a machine-learning model safely?

Only load model files from sources you trust, verify their integrity with a checksum or signature, and prefer formats with a non-executable loading path. For PyTorch, use weights_only=True where supported so the loader does not reconstruct arbitrary pickled objects.

Never miss an update

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