What is insecure deserialisation and how do I fix it?

Direct answer

It's when your application reconstructs an object from data a user controls, and the format allows that data to specify code. In Python it's `pickle` and `yaml.load`. The fix is not to sanitise the input, it's to use a format that can't express code. JSON, and `yaml.safe_load`.

Muhammad HasanUpdated

Serialisation turns an object into bytes; deserialisation turns bytes back into an object. The problem is that some formats can describe how to construct an object, which means the data can specify code to run.

Python pickle

# Vulnerable
data = pickle.loads(request.data)

Pickle's format includes instructions for calling arbitrary callables during reconstruction. Anyone who can supply the bytes can execute code as your process.

This isn't a bug in pickle. It's documented behaviour, and the standard library says plainly that it isn't secure against maliciously constructed data.

The fix is to use a different format:

data = json.loads(request.data)

JSON describes values, not construction. There's nothing to exploit because there's no mechanism to express it.

If you genuinely need to move Python objects between trusted services, sign the payload with HMAC and verify before unpickling. But "trusted" has to mean genuinely trusted. A cache, a queue or a session store that anyone can write to is not.

YAML

# Vulnerable
config = yaml.load(user_input)

# Safe
config = yaml.safe_load(user_input)

Full YAML supports type tags that instantiate arbitrary Python objects. safe_load restricts it to plain data types. Recent PyYAML versions warn or require an explicit loader, but older code is everywhere.

Other languages

Node. No native equivalent, and JSON.parse is safe. Risk comes from libraries that deserialise richer formats, node-serialize is the well-known one. And from prototype pollution, where a crafted __proto__ key in JSON corrupts object prototypes. Different mechanism, similar consequences.

Java. Native serialisation with a long history of gadget-chain exploits. Avoid ObjectInputStream on untrusted input; prefer JSON with an explicitly typed mapper.

PHP. unserialize() on user input, exploitable through magic methods. Use json_decode.

The pattern holds across all of them: the safe format is the one that can only express data.

Where it hides

Rarely a pickle.loads(request.body) sitting in a request handler. More often:

  • Session data. Frameworks that serialise sessions into cookies or Redis. If the store is writable or the cookie isn't signed, that's an entry point.

  • Caches. Objects cached as pickled bytes in Redis or memcached, where anyone with network access to the cache can write.

  • Message queues. Tasks serialised into a queue. Celery's default is JSON now, but older configurations used pickle.

  • File uploads. Model files, .npy arrays, and anything ML-adjacent, many of those formats are pickle underneath.

That last one matters more than it used to. Loading a model from an untrusted source is the same problem in a different costume.

What a scanner will tell you

The direct cases are findable. pickle.loads, yaml.load without a safe loader and unserialize are distinctive calls with traceable inputs.

How findable depends on the tool. In our RealVuln benchmark, insecure deserialisation is the class with the widest measured gap of any we broke out: LLM-based scanners recalled 100% of the labelled instances, rule-based tools 57%. Even on a bug with a named function at the centre of it, better than two in five went unreported by the pattern matchers.

The indirect route is harder again. A cache or session backend configured to use pickle in a settings file, several layers from where untrusted data enters, has no vulnerable-looking line at all. Grep your configuration for serialiser settings as well as your application code.

Go deeper

Fast remediation with Kolega

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free