Insecure deserialization gets less attention in day-to-day code review than injection or access control, partly because it is less common and partly because it looks harmless — a line of code that turns stored bytes back into an object. But when the format being deserialized is a native serialization format rather than plain data, and the bytes come from somewhere an attacker can influence, that one line can lead straight to remote code execution. Reviewers do not need to be exploit developers to catch this. They need to recognize a short list of dangerous function calls and ask where their input actually comes from.
Why deserialization can be dangerous
Plain data formats like JSON describe values — strings, numbers, arrays, objects — with no way to encode executable behavior. Native serialization formats are different: they can describe entire objects, including which class to instantiate and what should happen during that instantiation. If an attacker can control the serialized bytes an application deserializes, they may be able to construct an object graph that triggers arbitrary code execution the moment it is loaded, before the application ever gets a chance to validate it. This is what makes insecure deserialization categorically different from most input validation problems — the danger happens during parsing, not after.
Where it shows up in real codebases
- Session data stored using a language's native object serialization instead of a plain format, then read back on every request.
- Caching layers that serialize objects to store them and deserialize them on read, where the cache can be written to or poisoned by something other than fully trusted application code.
- Background job or message queue payloads deserialized into objects on the worker side, especially when the queue is reachable by more than one trusted service.
- Cookies or hidden form fields that store a serialized object directly, sent back to the server on the next request and deserialized without question.
- Configuration or plugin systems that load serialized objects from a file or remote location to support extensibility.
Function calls that deserve a second look
A short list of calls accounts for most real insecure deserialization findings, and reviewers can treat any of them as worth a closer look whenever the input is not fully trusted: pickle.loads and similar in Python, yaml.loadwithout an explicit safe loader, Java'sObjectInputStream.readObject, PHP's unserialize(), and Ruby'sMarshal.load. None of these are automatically wrong to use — they are wrong to use on data that originated outside a fully trusted boundary.
Safer alternatives reviewers should expect to see
The most reliable fix is to avoid native object deserialization for anything that touches untrusted input at all, and use a plain data format like JSON instead, parsed into known fields rather than arbitrary objects. Where a native format is genuinely required, safer variants exist: yaml.safe_load instead of yaml.load, and deserialization libraries that support an explicit allow list of classes that are permitted to be constructed, rejecting anything else. A reviewer seeing one of the risky calls above should ask whether the safer equivalent was considered, and if not, why not.
Trust boundaries matter more than the format itself
The same serialization call can be entirely safe or seriously dangerous depending on where its input comes from. Deserializing a value your own backend wrote to its own cache, using a key it controls, is a very different risk than deserializing a value pulled from a queue that any authenticated client can publish to, or from a cookie sent by the browser. Reviewers should trace the data back to its origin before deciding how much scrutiny a deserialization call deserves — the function name alone does not tell the whole story.
Reviewer checklist
- Identify every place in the diff that deserializes data using a native object format rather than a plain data format.
- For each one, trace the input back to its source and confirm whether any part of it could be influenced by a user, client, or external system.
- Where input is not fully trusted, confirm a safe-loading variant or class allow list is used, not the default unrestricted loader.
- Prefer plain data formats over native serialization wherever the use case allows it, especially for anything crossing a trust boundary.