| | |

Serialization 13 ๐Ÿ“„ Deserialization Security โ€” Arbitrary Code Execution and Gadget Chains

โฑ๏ธ Estimated read: 10 min | |

Deserialization is the inverse of serialization: bytes become objects. The security risk is not in the format itself but in the deserializer’s willingness to construct whatever type the payload names. When an attacker controls the bytes, they control which classes are instantiated, which constructors run, and which methods execute. The result is arbitrary code execution without a single line of attacker-supplied code in the applicationโ€”the code already exists on the classpath, and the payload chains it together.

This chapter covers the vulnerability class (CWE-502), the gadget chain mechanism that turns object construction into code execution, the language-specific risky APIs, and the mitigations that close the hole. The sections that follow walk through the gadget chain mechanism, the risky calls per language, and the allowlist-and-avoidance strategies that prevent the attack.

Key point: A deserialization vulnerability is only exploitable when a gadget chain existsโ€”a sequence of legitimate classes on the classpath that, when instantiated in a specific order, reaches a dangerous sink like Runtime.exec() or os.system(); the fix is to stop letting the payload choose the type.


a. The gadget chain mechanism

A gadget chain is a sequence of classes and methods that the deserializer invokes automatically. In Java, the chain typically starts with a class whose readObject method performs an action, continues through a map or collection whose entries trigger another method, and ends at a sink that executes the attacker’s command . The attacker does not need to upload code; the code is already present in the libraries the application depends on, and the payload arranges the calls.

The canonical example is the CommonsCollections chain. A HashSet containing a TiedMapEntry whose key is a LazyMap with a Transformer chain can trigger Runtime.exec() when the HashSet is deserialized, because readObject triggers hashCode(), which triggers the transformer chain . The chain is composed entirely of classes the application legitimately uses; the payload just orders them in a way the application never intended.

.NET has the same mechanism. The TypeConfuseDelegate gadget in ysoserial.net uses a ComparisonComparer whose delegate is a Func<string, string, int> pointing to Process.Start, packaged inside a SortedSet whose OnDeserialization callback triggers the comparison . The payload is a serialized object graph; the deserializer reconstructs it, and the reconstruction executes the chain.

The PHP equivalent is the POP (Property-Oriented Programming) chain. A class with a __destruct method that calls exec() can be reached through another class’s __toString or __wakeup method, which is triggered by unserialize() . The phpggc tool catalogs chains for Laravel, Symfony, Guzzle, Magento, and WordPress .


b. Risky APIs by language

The risky call is the one that lets the payload choose the type. Every language has at least one, and most have several.

In Java, ObjectInputStream.readObject() is the primary sink. Any class that implements Serializable can be instantiated, and its readObject, readResolve, readExternal, or readUnshared method runs. XMLDecoder and XStream.fromXML are also named by OWASP . The traffic signature is the byte prefix AC ED 00 05 or the Base64 prefix rO0 .

In .NET, BinaryFormatter is the canonical insecure deserializer. Microsoft’s documentation states it is insecure and cannot be made secure . SoapFormatter, LosFormatter, NetDataContractSerializer, and ObjectStateFormatter carry the same risk. Json.NET‘s TypeNameHandling setting is the JSON equivalent: when it is not None, the payload can name the type . The EasyFlow vulnerability (CVE-2026-102455) is an unauthenticated remote code execution through .NET deserialization, with the classic pattern: no input validation before reconstructing the object graph .

In Python, pickle.loads is the sink. The Python documentation says pickle can execute arbitrary code while loading, and yaml.load with the full loader is equally powerful . jsonpickle.decode is also named by OWASP . The pyquokka FlightServer vulnerability (CVE-2025-62515) is a direct pickle.loads on an action body received over the network, exploitable by any client that can connect .

In PHP, unserialize() on user data is the sink. The phpggc catalog lists dozens of chains for popular frameworks, including Laravel, Symfony, Guzzle, Magento2, and WordPress . The chain is triggered by magic methods: __destruct, __wakeup, __toString, or __call .

In Ruby, Marshal.load is the sink. The ERB vulnerability (CVE-2026-41316) bypasses a protection mechanism against malicious deserialization, and Rails applications that process Marshal data from insecure sources are exposed . The chain uses ERB’s template execution as the sink, reached through a gadget chain in ActiveSupport .


c. Mitigations: avoid, allowlist, and bound

The primary mitigation is to stop using native serialization for untrusted data. OWASP’s guidance is to prefer pure data formats such as JSON or XML with explicit types, because a data format cannot name arbitrary classes . JSON with TypeNameHandling.None in Json.NET, System.Text.Json in .NET, and yaml.safe_load in Python are the safe defaults.

When native serialization cannot be avoided, the second layer is an allowlist. Java’s serialization filters (ObjectInputFilter) restrict which classes can be deserialized, with limits on graph depth, array length, and stream size . The allowlist is stronger than a blocklist because it names what the application needs, not what attackers have already used . OWASP notes that filters do not make arbitrary untrusted deserialization safe; they reduce the attack surface when the format is unavoidable .

The third layer is resource bounding. A filter that only checks class names does not bound the size of the object graph, the depth of nesting, or the number of references. A payload can exhaust memory without executing code. The filter must set limits, and the input size must be capped separately because filters are not called for primitives .

The fourth layer is domain hardening. A class that must never be deserialized can define a final readObject() that always throws. Sensitive fields can be marked transient so they are not serialized. The resolveClass override is an illustration, not a full defense, because it does not check every object and proxy descriptors go through resolveProxyClass .


Complete Example Session

// ============================================
// PART 1: THE VULNERABLE PATTERN
// ============================================
import java.io.*;

// Reads whatever type the stream names
ObjectInputStream ois = new ObjectInputStream(untrustedStream);
Object obj = ois.readObject(); // gadget chain executes here

// ============================================
// PART 2: THE SAFE PATTERN (JSON WITH EXPLICIT TYPE)
// ============================================
// Jackson: no polymorphic type handling
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class); // fixed type

// ============================================
// PART 3: THE ALLOWLIST FILTER (JAVA)
// ============================================
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.User;java.lang.String;!*" // allow only these
);
ObjectInputStream ois = new ObjectInputStream(stream);
ois.setObjectInputFilter(filter);

// ============================================
// PART 4: DOMAIN HARDENING
// ============================================
public class SensitiveClass implements Serializable {
    private transient String secret; // not serialized
    private final void readObject(ObjectInputStream in) throws IOException {
        throw new InvalidObjectException("Not deserializable");
    }
}
# ============================================
# PART 5: PYTHON SAFE PATTERN
# ============================================
import json
import yaml

# SAFE: data-only format
data = json.loads(untrusted_json)

# SAFE: YAML safe loader
data = yaml.safe_load(untrusted_yaml)

# UNSAFE: pickle on untrusted input
# import pickle
# data = pickle.loads(untrusted_bytes)  # RCE

# ============================================
# PART 6: .NET SAFE PATTERN
# ============================================
// System.Text.Json: no type names by default
var user = JsonSerializer.Deserialize<User>(json);

// Json.NET: TypeNameHandling must be None
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.None
};

The parts covered the vulnerable Java pattern, the safe JSON pattern, the Java allowlist filter, domain hardening, the Python safe loaders, and the .NET safe defaults.


Quick Reference

Risky APIs by Language

LanguageRisky CallSafe Alternative
JavaObjectInputStream.readObject()JSON with fixed types, allowlist filter
.NETBinaryFormatterSystem.Text.Json
.NET (JSON)TypeNameHandling โ‰  NoneTypeNameHandling.None
Pythonpickle.loads, yaml.loadjson.loads, yaml.safe_load
PHPunserialize()json_decode()
RubyMarshal.loadJSON, safe YAML

Traffic Signatures

LanguageSignature
JavaAC ED 00 05 (hex), rO0 (Base64)
.NETBinaryFormatter stream patterns
PythonPickle opcodes

Mitigation Layers

LayerMechanism
AvoidData-only formats (JSON, safe YAML)
AllowlistSerialization filter with expected classes
BoundGraph depth, array length, stream size
HardenreadObject throws, transient fields

Best Practices

โœ… Do This:

// Use a serialization filter with an allowlist
ois.setObjectInputFilter(filter);                             // โœ…

// Fix the target type in code
mapper.readValue(json, User.class);                           // โœ…

// Set TypeNameHandling to None
TypeNameHandling = TypeNameHandling.None;                     // โœ…
# Use safe_load for YAML
yaml.safe_load(untrusted);                                    // โœ… 

# Use json.loads for data
json.loads(untrusted);                                        // โœ…

โŒ Don’t Do This:

// Read arbitrary types from untrusted stream
ois.readObject();                                             // โŒ

// Allow polymorphic type handling
TypeNameHandling = TypeNameHandling.All;                      // โŒ

// pickle.loads on network input
pickle.loads(untrusted);                                      // โŒ 

// unserialize() on user data
unserialize($_POST['data']);                                  // โŒ

Common Pitfalls

PitfallWhy It HappensFix
RCE through “internal” queueTrust boundary assumed, not verifiedTreat all cross-process data as untrusted
Allowlist bypassed by proxyresolveProxyClass not filteredFilter proxy descriptors
Filter does not bound memoryOnly class names checkedAdd graph/array/stream limits
Gadget chain survives patchOnly known chain brokenPatch underlying primitives, not just the published exploit
JSON still vulnerableTypeNameHandling not NoneSet to None explicitly

Real-World Examples

1. Java CommonsCollections chain

HashSet โ†’ TiedMapEntry โ†’ LazyMap โ†’ Transformer โ†’ Runtime.exec

2. .NET TypeConfuseDelegate

SortedSet โ†’ ComparisonComparer โ†’ Process.Start

3. PHP __destruct chain

unserialize โ†’ __destruct โ†’ exec

4. Python pickle RCE

class RCE:
    def __reduce__(self):
        return (os.system, ('ls -l',))

5. Java filter allowlist

ObjectInputFilter.Config.createFilter("com.example.User;!*");

6. .NET TypeNameHandling

TypeNameHandling = TypeNameHandling.None;

7. Python safe_load

yaml.safe_load(untrusted);

8. PHP json_decode

json_decode($input, true);

9. Ruby Marshal warning

# Avoid Marshal.load on untrusted data

10. ysoserial.net ViewState

ysoserial.exe -p ViewState --generator=<gen> --validationkey=<key>

Visual

The Gadget Chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PAYLOAD (serialized bytes)                             โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Deserializer reads type names from payload             โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Constructs object graph                                โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  readObject / __destruct / readResolve runs             โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Chain: A โ†’ B โ†’ C โ†’ sink (exec)                         โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Arbitrary code execution                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Trust Boundary

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  TRUSTED                    UNTRUSTED                   โ”‚
โ”‚                                                         โ”‚
โ”‚  In-process objects         Network input               โ”‚
โ”‚  Internal cache             Queue messages              โ”‚
โ”‚  Signed data                User uploads                โ”‚
โ”‚                                                         โ”‚
โ”‚  Native serialization OK    Native serialization NEVER  โ”‚
โ”‚  (if both ends trusted)     (use data-only format)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Mitigation Layers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  LAYER 1: AVOID                                         โ”‚
โ”‚  JSON, safe YAML, explicit types                        โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  LAYER 2: ALLOWLIST                                     โ”‚
โ”‚  Serialization filter with expected classes             โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  LAYER 3: BOUND                                         โ”‚
โ”‚  Graph depth, array length, stream size                 โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  LAYER 4: HARDEN                                        โ”‚
โ”‚  readObject throws, transient fields                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Patch Trap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CHAIN PUBLISHED                                        โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Maintainer patches one gadget                          โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Attacker reuses surviving gadgets                      โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  New chain works                                        โ”‚
โ”‚                                                         โ”‚
โ”‚  Fix: patch the primitives, not the published exploit   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Vulnerability classCWE-502 (Deserialization of Untrusted Data)
MechanismGadget chain of legitimate classes reaching a sink
Java sinkObjectInputStream.readObject()
.NET sinkBinaryFormatter, TypeNameHandling โ‰  None
Python sinkpickle.loads, yaml.load
PHP sinkunserialize()
Ruby sinkMarshal.load
Primary fixData-only formats with explicit types
Secondary fixAllowlist filter with resource limits
Patch rulePatch primitives, not published chains

Key takeaways:

  • The payload chooses the type, and the type is the attack. A deserializer that constructs whatever class the bytes name gives the attacker control over object construction. The fix is to fix the type in code, not let the data decide .
  • Gadget chains turn construction into execution. The chain is a sequence of legitimate classes already on the classpath. The attacker does not upload code; they arrange calls. Java’s CommonsCollections, .NET’s TypeConfuseDelegate, PHP’s __destruct chains, and Ruby’s ERB chains are all variations of the same mechanism .
  • Every language has a risky call and a safe alternative. Java has ObjectInputStream and JSON with fixed types. .NET has BinaryFormatter and System.Text.Json. Python has pickle and json. PHP has unserialize and json_decode .
  • Allowlists are stronger than blocklists. A blocklist of known-bad classes is always incomplete because new chains are discovered. An allowlist names what the application needs and rejects everything else .
  • Filters do not make untrusted deserialization safe. OWASP is explicit: filtering reduces risk but does not eliminate it. The primary control is to avoid native serialization for untrusted data entirely .

Remember: Deserialization is the moment bytes become objects, and objects can execute code. The vulnerability is not in the format; it is in the deserializer’s willingness to construct types the payload names. Use data-only formats for untrusted input, fix the target type in code, and if native serialization is unavoidable, put an allowlist filter with resource limits in front of it. Patch the primitives, not the published exploitโ€”an attacker who finds one broken link will replace it with a surviving one.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!