Insecure Deserialization: Risks and How to Prevent It
Insecure deserialization lets attackers turn untrusted data into arbitrary code execution, and here's how it happens and how to stop it.
Key takeaways
Insecure deserialization lets attackers turn untrusted data into arbitrary code execution, and here's how it happens and how to stop it.
On this page
Insecure Deserialization: Risks and How to Prevent It
Insecure deserialization is what happens when an application rebuilds objects from data it did not verify, letting an attacker's crafted byte stream trigger code execution during the rebuild itself. The bug lives in the gap between "receive bytes" and "inspect the result": native deserializers in Java, Python, PHP, and .NET run constructors, setters, and magic methods before your application logic ever sees the object, and an attacker who controls the input controls what runs.
What are serialization and deserialization?#
Serialization turns an in-memory object, a class instance with fields, methods, and internal state, into a flat sequence of bytes so it can be stored on disk, cached, or sent across a network. Deserialization is the reverse: take that byte stream and reconstruct the original object graph, with its type and internal state intact.
This pattern is everywhere. Session tokens, cached objects, queue payloads, and inter-service RPC calls all lean on serialization formats to move structured data around. Java has ObjectInputStream and Serializable. Python has pickle. PHP has serialize()/unserialize(). .NET has BinaryFormatter. Each one promises the same convenience: hand it an object, get bytes back, and later get the same object back from those bytes.
The convenience is also the problem. Object serialization formats do not just carry data, they carry type information and instructions for rebuilding that type. When the bytes come from a source you do not control, that source decides which classes get instantiated and which code paths run.
Why is deserializing untrusted data dangerous?#
A JSON parser reading {"name": "attacker"} can only produce strings, numbers, booleans, arrays, and objects made of those primitives. It has no concept of "now call this method." A native object deserializer is a different animal. To rebuild a Serializable object it has to invoke constructors, setters, and lifecycle hooks (Java's readObject(), Python's __reduce__, PHP's __wakeup() and __destruct()) as part of the reconstruction process, not after it.
A malicious payload does not need to exploit a bug in your business logic at all. It only needs to describe an object whose normal, intended behavior does something dangerous when constructed. Chain a few such objects together (a gadget chain) and the side effects compound into arbitrary command execution, all before your code gets a chance to check whether the deserialized value looks sane. That is why insecure deserialization routinely leads straight to remote code execution rather than something milder like corrupted data.
What does insecure deserialization look like in each language?#
- Java:
ObjectInputStream.readObject()on untrusted bytes is the classic entry point. Attackers rarely need a bug in your code; they need one exploitable class already sitting on the classpath (Apache Commons Collections and Spring have both supplied historical gadget chains) and can wire it into a working exploit with public tooling. - Python:
pickle.loads()on attacker-supplied data is explicitly unsafe. Python's pickle format can embed a__reduce__call that runs any importable function, includingos.system, at load time. - PHP:
unserialize()on user input has produced years of PHP object injection (POI) bugs, typically abusing__wakeup()or__destruct()magic methods to trigger file writes, SQL injection, or code execution. - Node.js: the
node-serializepackage'sunserialize()was shown to execute embedded JavaScript via a crafted_$$ND_FUNC$$_prefix in the serialized string, turning a cookie value into remote code execution. - .NET:
BinaryFormatterhas the same class of risk, and Microsoft's own guidance now recommends removing it from applications entirely rather than trying to sandbox its use, in favor of safer serializers.
How do attackers exploit deserialization?#
The reconnaissance is often boring. Attackers look for base64-looking blobs sitting in cookies, hidden form fields, or URL parameters, then decode them offline to check for the telltale headers of a serialized object (Java's aced0005 magic bytes, PHP's O:8:"classname" pattern, a pickled opcode stream). Once they confirm the format, they rarely need to hand-craft an exploit from scratch. Tools like ysoserial for Java generate ready-made gadget-chain payloads for dozens of known-vulnerable library combinations, and equivalent tooling exists for PHP and .NET. Insecure deserialization had its own entry in the OWASP Top 10 2017 as A8, and in the 2021 edition it was folded into the broader "Software and Data Integrity Failures" category, reflecting that the underlying problem (trusting bytes to describe their own safe reconstruction) applies beyond just object graphs.
How do you prevent insecure deserialization?#
The first fix is architectural: never feed data from an untrusted source into a native, unrestricted deserializer. If a value crosses a trust boundary, a user's browser, an external API, another team's queue, treat it as hostile input.
Prefer data-only formats for anything crossing that boundary. JSON, once parsed with a standard library parser, produces plain data structures with no notion of arbitrary type instantiation or method invocation. That single property closes off the whole class of gadget-chain attacks, which is why most modern APIs standardized on JSON over the wire instead of native object serialization.
# Unsafe: pickle reconstructs arbitrary Python objects,
# running __reduce__ during the load itself.
import pickle
def handle_request(request_data):
obj = pickle.loads(request_data) # attacker-controlled code path
return obj
# Safer: JSON only ever produces plain data structures.
import json
def handle_request(request_data):
obj = json.loads(request_data) # dict/list/str/int/bool/None only
return obj
If you genuinely need to move rich objects across a trust boundary, sign the payload and verify that signature before deserialization runs, not after. An HMAC or similar integrity check means a tampered payload gets rejected at the door, before any bytes reach the deserializer. Keep whatever deserialization library you use patched, since most gadget chains depend on a specific vulnerable class being reachable. And run deserializing processes with least privilege, so a gadget chain that slips through lands in a low-permission process instead of one that can read secrets or reach production systems.
None of this is exotic. It is the same trust-boundary discipline behind the rest of application security best practices: validate at the edge, prefer the least powerful format for the job, and assume anything you did not generate yourself is hostile. For the broader landscape this bug sits in, see the OWASP Top 10 explained.
The call we'd make#
Default to JSON or another data-only format for anything crossing a trust boundary, full stop. If a native deserializer has to touch external input, put a signature check in front of it and keep the process it runs in locked down, because gadget chains are a solved problem for attackers and an ongoing one for everyone still calling pickle.loads() or unserialize() on data they did not generate themselves.
Stay Updated
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
SSRF Explained: How Server-Side Request Forgery Works
SSRF tricks a server into making requests on an attacker's behalf, often reaching cloud metadata endpoints or internal systems the attacker could never hit directly.
Security Misconfiguration: The OWASP Category Nobody Talks About
Security misconfiguration quietly outranks flashier bugs as a top cause of breaches, yet teams rarely treat it as a real engineering problem.
More from DevOps
Explore more articles in this category
Business Logic Vulnerabilities: The Flaws Scanners Can't Find
Business logic vulnerabilities exploit legitimate application workflows rather than broken code, so scanners routinely miss them entirely.
GraphQL Security Best Practices
GraphQL's single flexible endpoint creates attack surfaces REST checklists miss, from introspection exposure to query depth and batching abuse.
Secret Scanning: Stop Secrets From Leaking Into Git
Secrets slip into git through habit and haste, and the only reliable fix is catching them before they're committed, not after.
You might have missed
Evergreen posts worth revisiting.