Why pickle.loads is dangerous, and where it hides in Python apps

Loading a pickle that an attacker could have influenced is unsafe: the Python documentation states that malicious pickle data can run code while it is being loaded, which is why the module's own warning is "only unpickle data you trust." The risk in a real application is rarely a visible call on a request body. It hides inside cache backends, task queues, session stores and machine-learning model files, where a developer never typed the word pickle. This article explains the risk at a level useful for defending against it, shows where it hides in first-party Python, lists safer formats, and gives a way to find every load.

The treatment here is deliberately defensive. The point is to recognise and remove the exposure, not to demonstrate an exploit.

The one-sentence reason

A pickle is not a passive data format the way JSON is. Restoring a pickled object can require importing a callable and invoking it, which is how pickle legitimately rebuilds instances of your own classes. The pickle documentation is explicit: unpickling "permits the unpickler to import and invoke arbitrary code," so the module "is not secure." This maps to CWE-502: Deserialization of Untrusted Data and sits inside OWASP's A08:2025 Software or Data Integrity Failures, which names CWE-502 among its mapped weaknesses.

The same caution applies to pickle's relatives. The standard library's marshal module "is not intended to be secure against erroneous or maliciously constructed data," and shelve is backed by pickle, so loading a shelf from an untrusted source carries the same risk.

The security question about any load is therefore not "is this data malformed" but "could anyone untrusted have influenced these bytes, directly or at some earlier point." If the answer is anything other than a firm no, pickle is the wrong format.

Where it hides in first-party Python

Few developers call pickle.loads() on a request body on purpose. The exposure comes from infrastructure that uses pickle under the hood, on data that turns out to be reachable by an attacker.

Caches

Several cache backends serialize values with pickle, so a read is an unpickle. Django's documentation warns that if a filesystem cache location sits inside a web-served directory, "an attacker who gains access to the cache file can not only falsify HTML content, which your site will trust, but also remotely execute arbitrary code, as the data is serialized using pickle" (Django cache framework). The memcached backends serialize with pickle as well. Anything that can write to the cache store, a shared Redis or memcached instance on a flat network, a writable file path, a key you let a user influence, becomes a path to a dangerous load.

Task queues and message brokers

Distributed workers serialize job payloads to send them over a broker. Celery's security guide states that "pickle is inherently insecure, and should be avoided whenever clients are untrusted or unauthenticated," and that its default serializer is JSON for that reason. A worker is also exactly the component an edge filter never sees, because it processes a job off the broker with no HTTP request in scope. If anyone who should not be trusted can place a message on the queue, a pickle serializer turns that into code execution on a worker.

The standard library has the same edge: multiprocessing notes that Connection.recv() "automatically unpickles the data it receives, which can be a security risk unless you can trust the process which sent the message."

Session stores

If session data is serialized with pickle and stored where integrity depends on a secret, a leak of that secret becomes code execution. Django deprecated its pickle session serializer in 4.1 and removed it in 5.0 "due to the risk of remote code execution" (Django 4.1 release notes). Its session docs explain the escalation for the signed-cookie backend: although the cookie is signed to prevent tampering, if SECRET_KEY leaks, "a SECRET_KEY leak immediately escalates to a remote code execution vulnerability" when pickle is the serializer. The bundled JSONSerializer avoids this entirely.

Machine-learning model files

The format that catches teams by surprise in 2026 is the model file. A great deal of saved-model tooling is pickle underneath:

  • NumPy's load warns that object arrays use pickle, "which is not secure against erroneous or maliciously constructed data," and recommends allow_pickle=False for untrusted sources.
  • pandas read_pickle warns that "loading pickled data received from untrusted sources can be unsafe."
  • joblib's load warns that it "relies on the pickle module and can therefore execute arbitrary code," so it "should never be used to load files from untrusted sources."
  • PyTorch changed the default of torch.load to weights_only=True in its 2.6 release, which it called "an important security improvement measure," precisely because the old default unpickled freely.

A model downloaded from a hub, pulled from object storage, or uploaded by a user is untrusted input. Treating a .pkl, .pt or .joblib file as data because it holds weights is the trap.

Safer options

In order of preference:

  1. Use a data-only format. For structured data, json carries no code and cannot invoke anything on load. It handles a smaller set of types, which is the point. For configuration, use yaml.safe_load, not yaml.load without a safe loader; current PyYAML requires the Loader argument specifically to stop the unsafe default.

    import json
    
    def read_payload(data: bytes) -> dict:
        obj = json.loads(data)
        if not isinstance(obj, dict):
            raise ValueError("expected a JSON object")
        return obj
    
  2. For ML weights, use a format that stores only tensors. safetensors was designed to hold weights without executing anything on load, and torch.load(..., weights_only=True) restricts loading to tensors rather than arbitrary objects.

  3. If you only need integrity, sign the data and keep it data-only. The pickle docs suggest signing with hmac if you must ensure data was not tampered with. Signing proves the bytes came from you; it does not make an attacker-chosen object safe, so sign a JSON payload rather than a pickle, and compare digests with hmac.compare_digest.

    import hashlib
    import hmac
    
    def verify(payload: bytes, signature: str, key: bytes) -> bool:
        expected = hmac.new(key, payload, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, signature)
    

If you genuinely cannot avoid pickle for a trusted internal channel, the documentation describes restricting what a custom Unpickler.find_class() will resolve to a small allowlist. Treat that as a last resort behind an authenticated, integrity-checked boundary, not as a way to make untrusted input safe.

How to find every load

Grep is the first pass. Search for pickle.load, pickle.loads, cPickle, marshal.load, shelve.open, torch.load, joblib.load, numpy.load, pandas.read_pickle, and yaml.load( without a safe loader. Bandit flags several of these directly. Then, for each hit, trace backwards: where do the bytes come from, and could anyone untrusted influence them at any point, including a value written to a cache or queue earlier.

That backward trace is the hard part, and it is where static search runs out. It can show you the call; it cannot tell you whether the bytes reaching it originated from a socket, a user-writable file, or a database row a different request planted. Runtime evidence answers the question that grep cannot. es-python, the EyalSec Python runtime, follows untrusted data through the program as it runs and checks it at the operation itself, so a deserialization load is reported only when data that entered from an untrusted source actually reaches it, with the path from where it entered. Because the check sits at the load rather than at the network edge, it fires on a worker pulling a job off a broker just as it does on a web request, and on a load deep inside a library the same as one in your own module. See How EyalSec works and the insecure deserialization overview.

The short version: pickle is a fine way to move objects between processes you fully trust, and a liability anywhere untrusted bytes can reach it. Pick a data-only format for everything crossing a trust boundary, sign what needs integrity, and know which of your model files, caches and queues are pickle underneath.

If you want to see which deserialization loads actually receive untrusted data on your own services, you can book a live demo.

EyalSec Pricing Docs Security Research Vulnerabilities Security glossary About Contact עברית Login Book a live demo