Insecure Deserialization: The Vulnerability Class Behind Several Major RCE Disclosures
The 2021 Log4Shell disclosure took down infrastructure across companies that had never even heard of Apache Log4j as a component in their stack. Underneath the specific logging library was a much older, broader problem that keeps resurfacing under different names every few years: trusting data you did not create enough to rebuild it back into a live object.
In this post
- What deserialization is actually doing, and why it needs care
- Gadget chains, at a conceptual level
- Why this kept producing major disclosures across languages
- What OWASP's guidance actually recommends
- Why this belongs in the same conversation as CSRF and XSS
- What to actually do about it
Deserialization vulnerabilities are one of those categories that is genuinely difficult to explain without either oversimplifying into meaninglessness or disappearing into the weeds of a specific language's object model. This article attempts the middle path: enough technical detail to be useful, without pretending the reader needs to understand Java's reflection API or PHP's magic methods to follow what is actually happening.
What deserialization is actually doing, and why it needs care
Serialization converts an in-memory object, with its state and structure, into a format that can be stored or transmitted. A file, a network payload, a session cookie. Deserialization reverses that, taking the stored representation and reconstructing a live object from it. This is completely ordinary, necessary functionality. Session state, cached data, message queues, and countless other everyday features depend on it.
The risk shows up specifically when the data being deserialized originates from somewhere untrusted. A user-submitted cookie, an API request body, a file upload, anything that could be manipulated by someone with different intentions than the application's author. In several serialization formats and languages, particularly Java's native serialization, PHP's unserialize(), and Python's pickle, the reconstruction process can trigger method calls automatically as part of rebuilding the object, sometimes before any application logic has a chance to validate what it is actually looking at.
This is not a bug in those languages. It is a design choice that made sense when the serialized data was assumed to come from a trusted source, often from the same application writing and reading its own state. The choice became a liability when serialization started being used for data crossing trust boundaries, which is common practice today even though it was not the original intended use case.
Gadget chains, at a conceptual level
The technique behind most serious deserialization exploits involves what security researchers call gadget chains: sequences of method calls, using classes already present in the target application's own codebase or its dependencies, that an attacker can trigger through carefully crafted serialized input. These chain harmless-looking individual operations into a sequence that ultimately achieves something the developer never intended, often arbitrary code execution.
The attacker is not injecting new code from outside. They are stitching together code the application already trusted and already had loaded, just in an order and combination nobody anticipated. Every individual method in the chain does exactly what it was written to do. The problem is the emergent behavior of chaining them together, which the application's author never considered because the combination seemed unlikely or impossible from their perspective.
This is worth sitting with for a moment, because it explains why the vulnerability class is so persistent across ecosystems. It is not a bug in any single class or method. It is an emergent property of having enough "gadgets," ordinary classes with ordinary side effects in their constructors or setters, sitting in an application's dependency tree, combined with a deserialization process willing to invoke them based on attacker-controlled input.
Nothing about any individual gadget is malicious. The danger only exists in how they can be assembled by someone who never needed to write a single line of new code to do it. This is why simply patching the "vulnerable class" in a widely used library does not solve the underlying issue. The class was not vulnerable. The way it could be chained was.
Why this kept producing major disclosures across languages
A quick, non-exhaustive look at how widely this vulnerability class has spread across ecosystems:
- Java. Apache Commons Collections, a hugely popular general-purpose library, was found to contain gadget chains usable against any application deserializing untrusted Java objects with that library present, affecting a wide range of unrelated applications that happened to share the dependency.
- PHP. Object injection through
unserialize()has appeared repeatedly across PHP applications and frameworks over the years, since PHP's magic methods like__wakeupand__destructcan be triggered automatically during deserialization. - Python. The pickle module's documentation itself carries an explicit warning against unpickling data from untrusted sources, because pickle's design allows arbitrary object reconstruction logic to execute as part of loading the data.
- .NET. Several serialization binders in the .NET ecosystem have had documented gadget chains, leading Microsoft to explicitly recommend against certain legacy serialization types for handling untrusted data in more recent guidance.
- Ruby. Similar issues have appeared in Ruby's Marshal module, where deserialization of untrusted data can trigger method calls during object reconstruction.
The pattern across every one of these is the same shape wearing different syntax. It is less a series of unrelated bugs and more a single design tension: deserialization frameworks were built for convenience and flexibility first, with the untrusted-input threat model bolted on afterward once real incidents forced the issue. The retrospective fix in each ecosystem has looked similar, adding warnings to documentation, adding allowlist-based restrictions, and eventually deprecating the most dangerous patterns in favor of safer alternatives.
What OWASP's guidance actually recommends
The OWASP Foundation dedicates specific, detailed guidance to this exact problem in its Deserialization Cheat Sheet. The core recommendations converge on a few themes:
- Avoid deserializing data from untrusted sources using native, code-executing serialization formats at all. Prefer data-only formats like JSON with a strict, predefined schema wherever functionally possible.
- Where native deserialization of external data is unavoidable, implement integrity checks, such as a cryptographic signature over the serialized payload, verified before any deserialization is attempted. Tampered payloads get rejected before they ever reach the vulnerable code path.
- Restrict deserialization to an explicit allowlist of expected classes, rather than allowing the process to instantiate any class present on the classpath. This directly limits the pool of available gadgets an attacker could otherwise chain together.
- Keep dependencies patched and minimized. A smaller, more current dependency tree means fewer known, published gadget chains sitting available for an attacker to reuse.
- Run deserialization in a low-privilege context when possible, so that even a successful exploit has a more limited blast radius.
| Defense | What it actually blocks |
|---|---|
| Use JSON with schema instead of native format | Removes the code-execution trigger entirely for most use cases |
| Cryptographic signature on payloads | Rejects tampered input before deserialization runs |
| Allowlist of classes | Limits the gadget pool an attacker can chain |
| Patched dependencies | Removes known, published gadget chains from the dependency tree |
| Low-privilege execution context | Reduces the impact of a successful exploit |
Why this belongs in the same conversation as CSRF and XSS
Deserialization vulnerabilities tend to be less visible in casual security discussions than cross-site scripting or the request forgery pattern covered in earlier posts on this blog, partly because exploiting one usually requires understanding the specific application's dependency tree in some depth, rather than a generic technique portable across any vulnerable site.
That obscurity has not made the impact smaller. Where XSS or CSRF typically compromise a session or trick a user's browser into an unwanted action, a successful deserialization exploit frequently leads directly to remote code execution on the server itself. That is a categorically more severe outcome, and part of why disclosures in this class tend to generate the kind of urgent, all-hands patching response that Log4Shell became known for industry-wide.
The three vulnerability classes also share a common root cause worth naming explicitly. All three happen when input that should have been treated as data ends up being treated as code. XSS does this with HTML and JavaScript, CSRF does this with request context, and deserialization does this with object state. The specific mechanisms differ, but the underlying mistake is the same category of error, which is why defenses across all three tend to converge on similar principles: validate input at trust boundaries, avoid mixing code and data, and prefer safe parsers to convenient ones when handling anything from outside the application.
What to actually do about it
For anyone maintaining an application that deserializes anything originating outside its own trust boundary, the practical takeaway is not to memorize specific gadget chains. Those change constantly as new ones get discovered and old ones get patched. It is to treat the deserialization step itself as a trust boundary deserving explicit validation, the same discipline already applied to database queries and rendered HTML output.
A few concrete steps for anyone in a position to make changes:
- Audit what serialization formats are in use. Identify every place the application deserializes data from a source that could be manipulated by an attacker.
- Replace native serialization with data-only formats where functionally possible. JSON with an explicit schema is the common choice. This alone eliminates the entire category for most use cases.
- Where native serialization must stay, add signature verification. Sign the payload when serializing with a key the application controls, and verify the signature before deserializing anything.
- Configure allowlists for the classes that can be deserialized. Most modern serialization libraries support this, and it prevents arbitrary class instantiation.
- Keep dependencies current and trim unused ones. Fewer dependencies means fewer potential gadgets, and updated dependencies mean fewer known chains.
- Run the deserialization step in a restricted context. Containerization, low-privilege users, and limited filesystem access reduce the impact if something slips through.
Deserialization is not a fundamentally broken idea. It solves real problems and is used safely in countless applications. The vulnerability appears when the trust assumptions baked into its design are not matched by the trust assumptions of the specific use case. Closing that gap is a matter of explicit validation at the trust boundary, not of abandoning the technique entirely. The OWASP Deserialization Cheat Sheet covers the specifics in more depth for anyone implementing defenses, and the general principle it returns to is the same one that applies to SQL injection, XSS, and CSRF: never treat input from outside the application as though it came from inside it.
Key Takeaways
- Deserialization reconstructs live objects from stored data, and some formats can trigger code execution during reconstruction
- Gadget chains use classes already present in the target application to achieve arbitrary code execution
- Java, PHP, Python, .NET, and Ruby have all had significant disclosures in this category
- Data-only formats like JSON with a schema eliminate the risk for most use cases
- Where native serialization must stay, signature verification and class allowlists are the primary defenses
- Deserialization is the same category of mistake as SQL injection and XSS: input treated as code instead of data
- The practical fix is treating deserialization as an explicit trust boundary, not memorizing specific gadget chains