A deserialization gadget chain is one of the more confusing ideas in application security, because it hides two different problems inside one attack. The first problem is that an app rebuilds objects from bytes it does not trust. The second is that the classes already sitting on the app’s classpath can be arranged so that the act of rebuilding them runs code the authors never intended. This post pulls those two apart and shows how they fit together across languages.
What a deserialization gadget chain actually is
Start with the word gadget. A gadget is an existing class that ships with the app or one of its libraries, whose automatically invoked methods do something an attacker can aim. One gadget reads a field and calls a method on it. Another takes a string and runs a system command. On their own they look harmless. A deserialization gadget chain wires several of them together so that reconstructing a single crafted object sets off a sequence that ends in remote code execution.
The key point is that no new code is uploaded. The attacker sends data. That data names classes the app already has, sets their fields to chosen values, and relies on the language rebuilding the object graph in a fixed order. The chain is assembled entirely from parts that were already there.
Two bugs, not one
People collapse these into a single thing and then cannot reason about the fix. Keep them separate.
- The deserialization bug. Untrusted bytes reach a deserializer. A cookie, a request body, an upload, or a message queue entry gets turned back into live objects. This is the flaw. It is covered in depth in our primer on what insecure deserialization is.
- The gadget chain. The payload that turns that flaw into code execution. It exists because of what is on the classpath, not because of your code. You can have the bug with no usable chain, and you can have dangerous gadgets that are never reachable because nothing deserializes untrusted input.
The danger appears when both are true at once: attacker reachable bytes reach a deserializer, and a working chain exists in the loaded libraries.
The bug: untrusted bytes reaching a deserializer
Every language has a function that takes bytes and returns an object. The problem is feeding one of those functions input a user can change. A rough shape, language aside:
data = request.cookies["session"]
obj = deserialize(data) // rebuilds whatever the bytes describe
use(obj)
If data is signed and verified first, the user cannot swap in their own object graph. If it is not, the user decides what gets built. That single decision is the whole opening.
The chain: classes already on the classpath
Rebuilding an object is rarely passive. Many runtimes call special methods while reconstructing a value, before your own code ever touches it. These are the entry points a chain starts from, and we cover them in magic methods in deserialization attacks. The attacker picks a first gadget whose automatic method fires on the way in, then chooses its fields so that method calls a second gadget, and so on down to a sink that runs a command or loads a class.
A made up three step chain, shown only as a concept:
GadgetA.readObject() -> calls toString() on a field
GadgetB.toString() -> looks up a value in a map, triggering transform()
GadgetC.transform() -> ends at a method that executes a command string
Each link is a normal method doing its normal job. The attack is the arrangement, not the code. This style of building an attack out of existing methods is called property oriented programming, and it generalizes far beyond any one language.
The attacker never writes the exploit code. They write the object graph that makes your own libraries run it in an order the authors never imagined.
The same shape across languages
This is not a single language flaw. The mechanics repeat everywhere objects are rebuilt from bytes, only the method names and formats change.
- Java. The classic native serialization chains, assembled from common library classes. See the Commons Collections gadget chain.
- Python. The pickle format runs a method that can be pointed at arbitrary callables. See Python pickle remote code execution.
- PHP. Object injection through cleanup and wakeup hooks. See PHP object injection.
- .NET. Legacy binary formatters that rebuild typed object graphs. See the .NET BinaryFormatter class.
Different words, one idea. Untrusted input decides which objects get built, and the building itself runs code.
Why these are hard to spot
Nothing in the vulnerable line looks wrong. A call to a deserialize function is ordinary. The gadgets live in third party code you never read. A text search for a bad function tells you a deserializer exists, but not whether attacker controlled bytes reach it, and not whether a chain is present in the exact library versions you ship. Answering those questions is a source to sink reachability problem over the whole classpath, which is the subject of how gadget chains are found.
How to prevent it
- Do not deserialize untrusted input with a format that can instantiate arbitrary types. Prefer plain data formats with a fixed, expected schema.
- If you must accept serialized objects, sign them and verify the signature before rebuilding anything.
- Restrict deserialization to an allowlist of expected classes, so an unexpected gadget type is rejected at the door.
- Keep libraries current and remove ones you do not use, since fewer classes on the classpath means fewer available gadgets.
For more bugs where ordinary looking input does extraordinary things, see our injection and input category.
The recurring theme is that the flaw is not in one line but in how untrusted input flows into a deserializer and what that deserializer can then reach. An autonomous researcher that reasons about how an application actually fits together, from the input a user controls to the method that finally runs, is built to find exactly this kind of flow. You can read how we think about that on our about page.
Frequently asked questions
What is a deserialization gadget chain?
It is a payload assembled from classes already present on an app’s classpath, arranged so that rebuilding a single crafted object runs a sequence of their automatically invoked methods. The sequence ends in code execution even though the attacker uploaded no new code.
How is a gadget chain different from the deserialization bug itself?
The deserialization bug is untrusted bytes reaching a deserializer. The gadget chain is the payload that turns that bug into code execution, and it exists because of the libraries loaded, not your own code. You need both for an exploit to work.
Do gadget chains only affect Java?
No. The same shape appears in Python pickle, PHP object injection, .NET binary formatters, and more. Only the method names and serialization formats change; the idea that rebuilding objects runs code is shared.
How do you prevent deserialization gadget chains?
Avoid deserializing untrusted input with formats that can instantiate arbitrary types, sign and verify any serialized objects you must accept, restrict deserialization to an allowlist of expected classes, and keep libraries trimmed and current so fewer gadgets are available.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
