Skip to content
RESEARCH INDEX BREACHROAD / INTELLIGENCE NOTE

Insecure Deserialization: When Data Becomes Code

Insecure deserialization in Java, .NET, and Python: identify trust boundaries, test without unsafe gadget chains, and remove the root cause of RCE.

PUBLIC RESEARCH
AUTHOR
/ Pentester (OSCP, PNPT)
PUBLISHED
9 May 2026
READING TIME
16 min read
TOPIC
AppSec
Insecure Deserialization: When Data Becomes Code

Insecure deserialization occurs when an application reconstructs an object from data it should not trust and the format or library lets that input influence the created type, object graph, or methods invoked during reconstruction. The outcome is not limited to remote code execution. It may include denial of service, clobbered security fields, authorization bypass, file access, and unexpected network connections.

In one paragraph: signing a payload does not turn a dangerous deserializer into a safe one. A signature provides integrity and origin under specific key assumptions, but it does not remove gadgets, key-management failures, dangerous data already stored in the system, or denial-of-service risk. The preferred strategy is to avoid native object deserialization across trust boundaries, use a data-only format with a strict schema, and map validated fields explicitly into permitted domain types.

MITRE defines this weakness as CWE-502: Deserialization of Untrusted Data. OWASP maintains a practical Deserialization Cheat Sheet, while Microsoft explicitly states that BinaryFormatter cannot be made safe for processing untrusted input.

Serializing data versus reconstructing behaviour

Serialization turns state into a representation that can be stored or transmitted. A safer model treats the representation as data: numbers, strings, lists, and explicitly defined records. A dangerous model attempts to reconstruct an arbitrary graph of objects, including class metadata, references, and hooks invoked during reconstruction.

That distinction matters. JSON does not execute code merely because it contains a type field, but a framework may add automatic polymorphic binding and instantiate a class named by that field. XML is a data format, yet a particular library may support types and callbacks that turn it into an attack surface. Security therefore depends on the format, configuration, library, and later use of the object.

LayerSecurity question
sourcecan a user, compromised host, or supplier modify the data?
formatcan input declare types, references, or construction behaviour?
deserializeris there a narrow class allowlist and graph limits?
classpath/runtimeare classes with useful side effects available?
post-processingdoes the application trust authorization fields or paths?

A sequence of classes whose methods or hooks create an unwanted effect during reconstruction is often called a gadget chain. A gadget does not have to be defined in the vulnerable function. It may be part of any library loaded in the process. This is why filtering a short list of known classes is fragile. As with JavaScript prototype pollution, the final impact depends on connecting a controllable source to a usable gadget.

Where trust boundaries really appear

Cookies, form fields, API bodies, and uploaded files are obvious sources. Less obvious paths are equally important:

  • queue messages written by another service;
  • cache or database records modified through SQL injection or a compromised account;
  • session files and desktop application save state;
  • backups and partner imports;
  • CI/CD artefacts and AI model checkpoints;
  • internal communication after migration from a desktop to a cloud architecture.

Microsoft’s BinaryFormatter security guide explains why supposedly internal data often crosses a trust boundary and how malware on one workstation could use deserialization to move laterally. The lesson is to model the full flow instead of applying an “internal equals trusted” label.

Java: ObjectInputStream and serialization filters

Java native serialization stores enough information to reconstruct objects. Review ObjectInputStream.readObject() calls and frameworks that invoke them indirectly. A Java stream may expose the AC ED 00 05 header or Base64 beginning with rO0, but the absence of those markers does not rule out other object formats.

Oracle documents Java Serialization Filters, introduced through JEP 290 and expanded by JEP 415. Filters can restrict permitted classes and graph complexity, including depth, reference count, and array size. This is valuable migration defence, but the filter should be contextual and narrow. A global allowlist containing “all application classes” may still include gadgets.

The safer destination is a DTO encoded in a data-only format, with explicit fields and no sender-controlled class selection. If compatibility requires a legacy format, isolate the parser and restrict classes, input size, execution time, and process privileges.

.NET: why BinaryFormatter was removed

Microsoft describes BinaryFormatter.Deserialize as unsafe for untrusted input and says it cannot be made secure. Starting with .NET 9, the in-box implementation was removed and use throws by default. The official BinaryFormatter migration guide documents the change and alternatives.

Do not simply move the same design to another deserializer with unrestricted polymorphism. Ask whether the data can select the type to instantiate. Prefer System.Text.Json with a concrete DTO, strict options, and deliberately designed polymorphism when it is genuinely required. Map the validated DTO into a domain object afterward.

An HMAC can protect a payload from unauthorized modification only when the key remains secret, is rotated, is used correctly, and is verified before deserialization. It does not protect against a malicious payload signed by a compromised producer or a historical object that becomes dangerous after a classpath update.

Python: pickle is an executable protocol

Python pickle is not an interchange format for untrusted data. Its model can describe how objects are reconstructed, including callable behaviour needed during creation. Python and OWASP guidance therefore warn against pickle.load() and pickle.loads() for data from an uncertain source.

Related risk appears in yaml.load() configurations that allow Python object construction, jsonpickle, and ML tooling built on top of pickle. Prefer JSON or a restricted safe YAML loader, validate a schema, and create domain objects manually.

This connects classic AppSec to the AI supply chain. A checkpoint downloaded from a repository crosses a trust boundary. Secure AI model loading requires checking its format, provenance, digest, and execution environment—not merely its file extension.

Testing without triggering a dangerous chain

Deserialization testing should minimise the possibility of code execution. Public tools that generate gadget chains may spawn processes, create network connections, or modify files. Do not run them against production simply to “prove RCE.”

1. Find the sink

During code review, identify deserialization APIs and wrappers. Record the format, polymorphism settings, allowed types, and data source. SAST, DAST, and IAST provide different evidence: SAST locates the call, IAST may confirm tainted flow, and DAST observes boundary behaviour.

2. Trace the trust boundary

Map the entire payload route: client, gateway, queue, cache, database, and worker. Identify who can modify the data and whether authentication is checked before parsing. Import and migration jobs deserve attention because they often run with elevated privileges.

3. Use a non-destructive canary

Prefer an unknown harmless type or a controlled test class that records an attempted construction inside the test process. A “class rejected” error may confirm an active filter. Timing differences and stack traces are clues, not sufficient proof of impact.

4. Verify resource limits

Test maximum byte size, graph depth, object count, timeout, and unknown-type behaviour. Deserialization can cause denial of service without any RCE gadget. Tests need hard caps and CPU/memory monitoring.

5. Assess classpath and process privileges

In a laboratory, determine whether classes with side effects are available and what the process can access. Even if a chain exists, a container without secrets, write access, or egress reduces impact. Secure by Design requires containment when a parser fails.

The target remediation pattern

  1. Replace the native object format with a representation that has no executable semantics.
  2. Define a versioned, strict schema with byte and depth limits.
  3. Parse into a simple DTO without client-controlled authorization fields.
  4. Validate types, ranges, relationships, and business rules.
  5. Construct a new domain object explicitly instead of restoring an arbitrary graph.
  6. Run the parser with minimal privileges, no unnecessary egress, and no unrelated secrets.
  7. Record rejection telemetry without logging full sensitive payloads.
ControlWhat it reducesWhat it cannot guarantee
data-only formatexecutable format semanticscorrect post-parse logic
strict schemaunknown fields and typesabsence of business flaws
class allowlistaccess to many gadgetssafety of allowed classes
graph limitssome denial-of-service pathsabsence of side effects
signature/HMACunauthorized modificationsafety of signed content
sandboxblast radiusremoval of the vulnerability

Migrating without stopping the system

Inventory all producers and consumers of the legacy format. Introduce a new versioned representation and a temporary dual-read period, but write only the new format. Measure legacy-path use, migrate stored data, and remove fallback on a defined date. Do not leave a “temporary” legacy import endpoint without authentication and limits.

Negotiate message versions as part of the service contract, not through runtime class names. Compatibility tests should exchange example records rather than serialized objects tied to a particular library build. This also improves software supply-chain security, because a dependency update cannot silently change the meaning of a historical object graph.

Reporting the finding accurately

The report should name the sink, format, data source, type configuration, available classpath, process privileges, and confirmed effect. Separate:

  • confirmed deserialization of untrusted data;
  • a potential gadget chain inferred from dependency versions;
  • a confirmed effect in an isolated laboratory;
  • maximum impact derived from service permissions.

Do not label every deserialization finding as RCE automatically. Conversely, the absence of a public gadget chain does not make the design safe: a future dependency may introduce one. The architectural cause remains.

Engineering checklist

  • Does native object deserialization occur across any trust boundary?
  • Can the sender choose a class or polymorphic type?
  • Does the parser cap bytes, graph depth, object count, and processing time?
  • Is message authentication verified before parsing?
  • Does the DTO reject unknown fields and client-supplied authorization state?
  • Does the parser process have least privilege and constrained egress?
  • Does the migration plan include a date for removing legacy fallback?
  • Do regression tests cover unknown types, oversized graphs, and old payloads?

Insecure deserialization is fundamentally a trust-boundary failure. If an external representation can decide which objects exist and what happens while they are constructed, data begins to behave like code. Book a web and API penetration test — we will trace the full data flow and propose a migration that removes the cause rather than one gadget chain.

SHARE / COPY