Skip to content
pwnsy
web-securityadvanced#deserialization#web-security#owasp#rce#appsec

Insecure Deserialization: Object Injection & Gadget Chains

Why deserializing untrusted data is dangerous, how gadget chains turn it into remote code execution, and safer serialization alternatives.

Applications constantly turn objects into bytes and back again: to store a session, to pass a message between services, to cache a result, to send a token to a client. The conversion out is serialization, the conversion back is deserialization, and the second half is where a specific and severe class of vulnerability lives. When a program deserializes data an attacker controls, the attacker gains influence over what objects the program builds and what code runs while building them.

This is one of the harder web vulnerabilities to reason about, because the payload rarely looks like an attack. It looks like a normal serialized object. The damage happens during reconstruction, deep inside library code the developer never wrote and rarely reads. Done right by an attacker, it produces remote code execution from nothing more than a modified cookie.

What serialization actually does

Serialization flattens a live object, its type, its fields, and their values, into a stream of bytes that can be stored or sent. Deserialization reads that stream and rebuilds an equivalent object in memory. The critical detail is that many serialization formats encode the object's type, and the deserializer will instantiate whatever type the stream names, then populate it and often run initialization or cleanup logic as part of the process.

That last part is the whole problem. If reconstruction can create arbitrary object types and trigger their methods, and the attacker controls the stream, then the attacker controls which objects get created and which methods fire. They do not need to inject new code. They compose existing code into a harmful sequence.

Why untrusted serialized data is dangerous

Consider a session stored client-side as a serialized object inside a cookie. The server deserializes it on every request to restore the session. If that cookie is not integrity-protected, the attacker rewrites it. A naive attack just changes a field, like flipping a role value from user to admin. That is the mild case.

The severe case exploits the reconstruction machinery itself. Native serialization formats often invoke special methods automatically during or after deserialization: constructors, initializers, and cleanup hooks defined on the object's class. The attacker supplies a stream describing an object of some class whose reconstruction hook does something useful to them, and it runs the moment the server deserializes.

ImpactHow it arises
Tampering / privilege escalationModifying fields of a deserialized object that the server trusts
Denial of serviceObjects whose construction consumes huge memory or CPU
Server-side request forgeryAn object whose reconstruction opens a network connection to an attacker-chosen host
Remote code executionA gadget chain that reaches a method executing a command

Gadget chains

Remote code execution rarely comes from a single method. It comes from a chain. A gadget is a method already present in the application or its dependencies that does something interesting when invoked with the right state. A gadget chain links several of them: the reconstruction of the outer object triggers a method that calls another method that calls another, and by the end the sequence reaches something dangerous, like reflection that loads a class or a call that runs a system command.

The attacker writes no code that runs on the server. Every gadget is legitimate library code that was already there. The attacker's only artifact is the serialized object graph that, when rebuilt, sends control down the chain. Public tooling has catalogued reusable gadget chains for popular libraries, which is why a single vulnerable deserialization call in a common framework can be exploitable with an off-the-shelf payload.

This is also why the bug resists the usual code review instincts. A reviewer looking at the vulnerable line sees a single call that deserializes a byte stream, which looks innocuous. The exploit is nowhere near that line. It is distributed across methods in libraries the reviewer will never open, assembled by an object graph that exists only in the attacker's payload. There is no dangerous-looking function to flag, no string concatenation to spot, no obvious injection point. The danger is structural: the mere act of reconstructing an attacker-chosen object graph is the vulnerability, and it hides in plain sight as a routine call.

The dependency you never audited is the weapon

Gadget chains live in your dependencies, not your code. You can write a perfectly clean class and still be exploitable, because the chain is assembled from methods in a logging library, an ORM, or a collections utility three levels down your dependency tree. This is why "we validated the object's fields" is not a defense. The dangerous behavior fires during reconstruction, before your validation ever runs.

Where insecure deserialization hides

The vulnerability appears wherever a program deserializes data that crossed a trust boundary. The tell is a serialized blob in a place a client or peer can modify:

  • Session objects serialized into cookies or hidden form fields
  • Authentication or state tokens that are serialized objects rather than signed claims
  • Cached objects in a shared cache that other tenants can influence
  • Messages on a queue consumed by a worker that deserializes them
  • API parameters that accept a serialized object
  • Inter-service calls using a native binary format over the wire

The pattern to watch for is a native, type-carrying serialization format applied to data that originated outside your trust boundary. Binary object formats that encode arbitrary types are the highest risk. Formats meant purely for data, like JSON, do not instantiate arbitrary application classes and so do not carry the same reconstruction risk on their own.

Safe alternatives and defenses

The most durable fix is architectural: do not deserialize untrusted data using a format that can instantiate arbitrary types. Everything else is mitigation on top of a dangerous choice.

Prefer a pure data format

Move to a serialization format that transports data, not live objects. JSON, or a schema-defined format like Protocol Buffers, describes values and structure. Your code reads those values and constructs known types explicitly, so the attacker never gets to name a type or trigger a reconstruction hook. This removes the gadget-chain class of bug rather than trying to contain it. See our secure coding practices guide for how this fits a broader input-handling discipline.

Sign serialized payloads you must round-trip

If a serialized object must go to a client and come back, protect its integrity. Attach a message authentication code computed with a server-side secret and verify it before deserializing. If the signature does not match, reject the payload without parsing it. This stops tampering, though it depends on the secret staying secret and does not help if the deserialization is reachable by other untrusted paths.

Allowlist permitted classes

Where a native format cannot be avoided, constrain deserialization to a strict allowlist of expected classes and reject anything else before reconstruction proceeds. This shrinks the attack surface, but it is fiddly to implement correctly and easy to bypass if the allowlist is too broad or a permitted class is itself a gadget entry point. Treat it as a hardening layer, not a primary control.

Isolate and monitor

Run deserialization in a low-privilege context so a successful chain lands somewhere with limited reach, and log deserialization failures, which often signal probing. Because a successful chain frequently ends in command execution, the same discipline that protects against command injection and server-side template injection applies to the aftermath.

Change the format itself

Adding class allowlists and signatures to a native binary deserializer is patching a hole in a dangerous wall. The stronger move is to stop deserializing untrusted native objects at all and switch the boundary to JSON or a schema format where the attacker cannot name a type or trigger a reconstruction hook. Redesign the interface once and the entire vulnerability class disappears.

How to defend against insecure deserialization

  1. Do not deserialize untrusted data with native object formats. Make this a hard rule. Any serialized object that crossed a trust boundary is untrusted.
  2. Use data-only formats with explicit construction. Accept JSON or a schema-based format, read the fields you expect, and build known types yourself.
  3. Integrity-protect any serialized payload that round-trips through a client, verifying a signature before parsing.
  4. Allowlist deserializable classes where a native format is unavoidable, and keep the list as small as possible.
  5. Keep dependencies patched, since gadget chains are discovered in libraries over time and updates remove known ones.
  6. Run deserialization with least privilege and monitor for deserialization errors that indicate probing.

Because a working chain so often ends in code execution, the attacker's next step is delivery: getting the crafted payload into your parser, frequently via a file, a token, or a queue message. Our Exploit Intelligence dashboard tracks which deserialization and object-injection flaws are being actively exploited, so you can prioritize the libraries and frameworks under real pressure instead of treating every advisory the same.

Insecure deserialization is a quiet vulnerability with a loud outcome. The payload looks like ordinary data, the dangerous work happens inside code you never wrote, and the result can be full control of the server. The path out is to stop trusting serialized objects from outside your boundary and to move those interfaces to formats that carry data instead of behavior.

A worked walkthrough of an object-injection chain

It helps to trace the shape of an attack once, in plain terms, without inventing a specific vulnerability. Picture an application that stores a shopping session client-side. The session is a live object with a few fields: a user identifier, a cart, and a timestamp. To persist it, the framework serializes that object into a byte stream, encodes it, and drops it into a cookie. On the next request the framework reads the cookie, decodes it, and deserializes the byte stream back into a session object.

Now trace what the attacker sees. The cookie is a blob they can read and rewrite. If the format is a native, type-carrying one, the first field of that blob effectively names a class. The attacker changes that name. Instead of describing a session object, the blob now describes an object of some other class that happens to live in the application's dependency tree. When the framework deserializes the cookie, it does exactly what it was told: it instantiates the class the blob names and populates the fields the blob supplies.

The next link is the reconstruction hook. Many classes define a method that runs automatically when an instance is rebuilt from bytes, a place meant for validating fields or restoring transient state. The attacker chooses a class whose hook reads one of its own fields and passes it to another method. That field, of course, is also attacker-supplied. So the hook fires, reads the attacker's value, and hands it to the next gadget. That gadget might invoke a method by name using reflection, and the name is again a field the attacker set. Step by step, control walks from a harmless-looking cookie into a call that loads a class, runs a template, or executes a command.

The important thing about this trace is that every method along the way is ordinary library code. Nobody wrote a malicious function. The attacker assembled a path through code that already existed, using nothing but the field values encoded in the object graph. That is the entire mechanism, and it is why the fix has to be structural. The technique corresponds to MITRE ATT&CK Exploitation for Client Execution and maps to CWE-502, Deserialization of Untrusted Data.

Detection signals

Insecure deserialization is quiet by design, so detection leans on knowing where serialized data enters and watching those points closely. A few concrete signals separate real exposure from theoretical worry.

Look first for the raw material. Base64 or hex blobs in cookies, hidden form fields, authentication tokens, cache entries, and message-queue payloads are the tell. A native serialization format usually carries a recognizable header or magic bytes at the start of the stream, so a decoded value that begins with a consistent prefix and then names class-like strings is a strong indicator that the application round-trips live objects rather than plain data.

At runtime, watch for deserialization errors in application logs. An attacker probing a parser generates malformed streams, and those throw type or class-resolution exceptions. A sudden rise in deserialization exceptions, especially ones naming unexpected classes, is one of the clearest live signals that someone is testing the surface. Unusual class-resolution attempts, where the runtime is asked to load a class that has no business appearing in a session or a token, deserve an alert on their own.

Behavioral signals sit downstream of a successful chain. Because chains so often terminate in command execution or an outbound connection, a worker process that suddenly spawns a shell, resolves an unexpected hostname, or opens a socket right after consuming a serialized message is worth treating as an incident. Correlating the timing of a deserialization event with a new child process is a high-value detection.

SignalWhere to lookWhat it suggests
Serialized blob a client can editCookies, hidden fields, tokens, queue messagesAn exposed deserialization boundary
Native-format magic bytes after decodingDecoded cookie or token valuesLive objects being round-tripped, not data
Spike in class-resolution exceptionsApplication and framework logsActive probing of the parser
Unexpected class names in errorsDeserialization stack tracesAttempted object injection
Process spawn or outbound socket after a deserializeHost and network telemetryA chain that reached execution
Instrument the parser directly

A web application firewall struggles here because a malicious serialized object looks like a normal one. The higher-value control is instrumentation at the deserialization call itself: log every class the runtime is asked to resolve during reconstruction, alert on any class outside a known-good set, and treat a resolution failure as a probing signal rather than a routine error.

Deserialization sits in the same family as other injection classes, and placing it beside them clarifies what makes it distinct.

TechniqueAttacker suppliesWhere the damage happensTypical fix
Insecure deserializationA crafted object graphDuring reconstruction, inside library codeStop deserializing untrusted native objects
Command injectionA string that reaches a shellAt the point the command runsAvoid shells, pass arguments as arrays
SQL injectionA string that reaches a queryAt query executionParameterized queries
Server-side template injectionTemplate syntax in inputWhen the template engine rendersKeep user input out of template code

The common thread is a trust boundary crossed by data that then gets interpreted as more than data. What sets deserialization apart is the distance between the vulnerable line and the payload. In SQL and command injection the dangerous sink is visible and local. In deserialization the sink is spread across methods in dependencies, and the single visible line, the call that reads a byte stream, looks benign. That distance is why static review and simple input filtering both underperform against it.

Common misconceptions

A few beliefs make this bug more dangerous than it needs to be, because they lead teams to defend the wrong thing.

"We validate the object's fields after loading, so we are safe." Validation runs after reconstruction, and the dangerous behavior fires during reconstruction. By the time your validation code sees the object, the reconstruction hooks and the gadget chain have already executed. Field validation protects against tampering with values you trust, and it does nothing against a chain that reaches code execution before your first check.

"Our own classes are clean, so we have nothing to worry about." The gadgets live in dependencies. You can audit every class you wrote and still be exploitable through a chain assembled from a logging framework, an object-relational mapper, or a collections utility deep in the tree. The relevant attack surface is the transitive dependency graph, not the application code.

"It is only a problem if we deserialize user input directly." The boundary is anywhere untrusted data enters, which includes caches other tenants can influence, queues fed by upstream systems, and inter-service messages. A serialized object that arrives from a peer service you do not fully control is untrusted for this purpose.

"Encrypting the blob fixes it." Encryption hides the contents and can prevent tampering when the key stays secret, which helps. It does nothing if the deserialization is reachable by another untrusted path, and it fails entirely if the key leaks. Encryption is a tampering control layered on a dangerous design, and the design is what carries the risk.

How the problem evolved

Native serialization was designed in an era when the network was assumed to be more trusted and convenience was the priority. The formats made it trivial to move a live object across a boundary and reconstruct it on the other side, complete with automatic reconstruction hooks. That convenience is exactly what made them dangerous once untrusted data started flowing into them.

The class of bug moved from obscure to mainstream when researchers published reusable gadget chains for widely used libraries and packaged them into tooling. Once a chain for a popular framework was public, a single vulnerable deserialization call anywhere that framework was used became exploitable with an off-the-shelf payload. That shifted the economics: an attacker no longer needed to discover a chain, only to find a place where untrusted data reached a native deserializer.

The defensive response followed two tracks. Libraries added allowlisting hooks and safer defaults, and guidance moved toward avoiding native deserialization of untrusted data entirely. The durable lesson from that history is that patching individual gadgets is a losing game, because new chains keep appearing, while changing the boundary format removes the whole class at once.

Frequently asked questions

Is deserialization always dangerous? No. Deserializing data you fully trust, or using a data-only format like JSON that cannot instantiate arbitrary application classes, does not carry the gadget-chain risk. The danger is specifically native, type-carrying formats applied to data that crossed a trust boundary.

Does using JSON make me completely safe? JSON removes the core reconstruction risk because it describes values and structure, not live objects, so a parser will not instantiate an arbitrary class from it. You still have to construct your own objects from those values carefully, and libraries that add type information on top of JSON to support polymorphic deserialization can reintroduce the problem. Plain JSON with explicit, known types is the safe path.

Can a web application firewall stop this? Not reliably. A malicious serialized object is structurally similar to a legitimate one, so signature-based filtering catches only known payloads and misses novel object graphs. Instrumenting the deserialization call and allowlisting classes is far more effective than perimeter filtering.

Why does removing the dangerous classes not fix it? Removing a known gadget helps against one published chain, and new chains form from different combinations of methods as dependencies change. Pruning gadgets is a hardening measure with a short shelf life. Avoiding untrusted native deserialization is the control that holds.

What is the single most effective fix? Move the boundary to a data-only format and construct known types yourself. That one architectural change removes the ability of an attacker to name a type or trigger a reconstruction hook, which is the root of the entire vulnerability class.

How do signed payloads fit in? Attaching a message authentication code to a serialized payload and verifying it before parsing stops tampering, so an attacker cannot swap in their own object graph. It depends on the signing secret staying secret and does not help if the deserialization is reachable through another path, so treat it as one layer rather than the whole answer.

Sources & further reading

Sharetwitterlinkedin

Related guides