Insecure deserialization: how it works, how to test it and how to fix it

Insecure deserialization happens when an application rebuilds objects from data an attacker can change. The data can make the app create unexpected classes and run code that already exists in it. CWE-502 covers it. Fix it by not deserializing untrusted native formats, using JSON with a schema, restricting types with an allowlist and signing data you must store.

Insecure deserialization is a flaw where an application rebuilds objects from data that an attacker can read or change. CWE-502, "Deserialization of Untrusted Data", describes it as a product that "deserializes untrusted data without sufficiently ensuring that the resulting data will be valid." Depending on the language and the classes on the server, the result can be denial of service, a bypass of access control or remote code execution. OWASP covers it under A08:2025 Software or Data Integrity Failures.

What is the difference between serialization and deserialization?#

Serialization converts an object and its fields into a flat format, such as bytes or text, that you can store or send. Deserialization reads that format and rebuilds the object. PortSwigger puts it this way: serialization turns complex data structures into a flatter format, and deserialization restores the stream to "a fully functional replica of the original object." Ruby calls this marshalling and Python calls it pickling.

The risk starts when the format can name a class. If the bytes say which class to create, the person who controls the bytes also controls which class the application instantiates. PortSwigger notes that attacks can finish before deserialization ends, so a check on the object afterward is often too late. OWASP's .NET section makes the same point: a check after deserializing is too late because execution may already have happened.

How does a deserialization attack work?#

The attacker changes the serialized data so that reading it starts code that already exists in the application or its libraries. Such code is called a gadget, and a chain of gadgets can turn a harmless-looking object read into a dangerous action. The attacker does not upload new code. The application loads the classes, and the data chooses which ones run.

Microsoft gives an analogy for .NET: calling BinaryFormatter.Deserialize on a payload is like interpreting the payload as a standalone executable and launching it. The Python documentation says malicious pickle data can "execute arbitrary code during unpickling".

Illustrative cookie that carries an object: Harbor Notes

This is a demonstration. No real customer system was tested. Harbor Notes is a fictional app and example.com is a reserved example domain.

After login, the fictional server sets a cookie that holds a serialized session object, encoded in Base64:

GET /dashboard HTTP/1.1
Host: app.harbor-notes.example.com
Cookie: session=rO0AB...(Base64 text, shortened)

The value starts with rO0. OWASP lists rO0 as the Base64 form of the Java serialization header AC ED 00 05. The tester has now learned two things without sending any harmful data: the server reads a Java object from a value the client controls, and the format lets the stream pick the class.

A tester stops here for the first finding. The format and the trust boundary are enough to report a high-risk design. Whether it leads to code execution depends on which libraries the app ships, and the lab proves that only with a harmless signal, covered below.

What are the main variants?#

The variants differ by language and by library. The table shows the unsafe calls that OWASP, Microsoft, the Python documentation and the PHP manual name, and the usual replacement.

Stack Unsafe on untrusted input Safer choice
Java ObjectInputStream.readObject, XMLDecoder, XStream fromXML on old versions JSON with DTOs, or an ObjectInputFilter allowlist
Python pickle.loads, yaml.load with an unsafe loader, jsonpickle.decode json.loads, yaml.safe_load
PHP unserialize() json_decode()
.NET BinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter, ObjectStateFormatter System.Text.Json, XmlSerializer, DataContractSerializer
JSON libraries Json.NET with TypeNameHandling other than None, old fastjson with autotype Plain data types, no class names in input
Node.js Any library that revives functions or class names from input JSON.parse plus schema validation

OWASP also lists library defaults. It says fastjson2 (autotype off), Jackson (no polymorphism), Kryo 5 and later, and XStream 1.4.17 and later are safe by default. SnakeYAML needs SafeConstructor. Check the Node.js row against your own dependency list, because the OWASP sheet does not name Node.js libraries.

Which real incidents involved deserialization?#

Two NVD records show the pattern. Both are old, and both are in products that many teams ran for years.

CVE Published Product What NVD says Rating
CVE-2015-4852 2015-11-18 Oracle WebLogic Server Remote attackers can run commands through a crafted serialized Java object in T3 protocol traffic to TCP port 7001. NVD maps it to CWE-502. CISA added it to its Known Exploited Vulnerabilities catalog on 2021-11-03
CVE-2017-9805 2017-09-15 Apache Struts REST plugin The plugin uses an XStream handler for deserialization "without any type filtering", which can lead to remote code execution through XML payloads. CVSS v3.1 8.1 High

The second row shows the fix in one phrase: type filtering. The framework read XML and let the input name the types. Both records are from NVD, checked on 9 November 2026.

OWASP A08:2025 also includes a scenario for the same class of flaw: a Spring Boot application that passes serialized user state with each request, where an attacker spots the rO0 Java signature and gains remote code execution.

How do penetration testers detect and prove it?#

Testers look for serialized data in places the client controls, confirm the server reads it, and prove the effect with a harmless signal. They never need a command shell to report the flaw. The Excession Labs method follows the evidence labels in our methodology: each finding links to requests and responses, and the lab drops or downgrades a finding when a control shows the first result was misleading.

Testing needs written authorization from the system owner. Without it, do not test.

  1. Find candidates. Check cookies, hidden form fields, headers, API bodies, message queues and uploaded files. OWASP lists these signatures: hex AC ED 00 05, Base64 rO0 and content type application/x-java-serialized-object for Java; Base64 gASV or a trailing . for Python pickle; AAEAAAD/////, TypeObject or $type: for .NET.
  2. Run a control. Send the original value back unchanged and note the response. Then send a value that is valid in format but wrong in content, such as a changed field. A changed error message shows the server parses the data.
  3. Read back the effect. If a field such as a role or price lives in the object, change it, resubmit and read the state back from a second session. This proves a trust failure without any code execution.
  4. Use a harmless signal for code paths. With written scope, a tester can use a payload that only makes the server resolve a hostname the tester owns. A lookup on that host shows the server processed the class. PortSwigger's materials describe tooling for this approach. The lab uses the signal, records the time and the source address, and stops.
  5. Record limits. Say what you did not test. A found format does not prove code execution, and the report labels it as inferred until a signal confirms it.

Evidence for a finding

Keep the original and changed values, the full request and response for each, the control result, the timestamp and the callback log if you used one. The report states which parts were observed and which are inferred.

A scanner finds some of these flaws by matching signatures, but it cannot say whether a class on the classpath makes the flaw useful. A tester reads the application's libraries and chooses the safest proof.

How do you prevent insecure deserialization?#

Do not deserialize untrusted data with a format that can name classes. OWASP's first advice is to use pure data formats such as JSON or XML, ideally with data-transfer objects, and to accept only signed data. If you must keep a native format, restrict the types and the size before you read.

Java#

Replace ObjectInputStream with JSON and a DTO where you can. If you cannot, set a filter on the stream before the first read. The Oracle documentation shows the pattern syntax: the first match wins and a final !* rejects everything not listed.

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "maxdepth=10;maxrefs=1000;maxbytes=65536;maxarray=10000;" +
    "com.example.model.*;java.base/*;!*");

try (ObjectInputStream ois = new ObjectInputStream(input)) {
    ois.setObjectInputFilter(filter);   // set before readObject
    Object obj = ois.readObject();      // throws InvalidClassException if rejected
}

For domain classes that should never be deserialized, OWASP suggests a final readObject() that always throws. Mark sensitive fields private transient. OWASP warns that "filtering does not make arbitrary untrusted deserialization safe", so treat the filter as a second layer.

Python#

The pickle documentation says to never unpickle data that could come from an untrusted source. Use JSON, and yaml.safe_load for YAML.

import json

data = json.loads(request_body)          # data types only, no code
name = str(data["name"])                  # validate each field

If you must unpickle your own stored data, restrict globals with a custom Unpickler.find_class() that forbids everything except a short list, as the documentation shows, and sign the data with hmac.

PHP#

Use json_decode() for anything a user can touch. The manual says not to pass untrusted input to unserialize() "regardless of the options value of allowed_classes".

$value = json_decode($input, true, 16, JSON_THROW_ON_ERROR);

// Stored data you signed yourself only:
if (hash_equals(hash_hmac('sha256', $blob, $key), $mac)) {
    $value = unserialize($blob, ['allowed_classes' => false]);
}

C# and .NET#

Microsoft says BinaryFormatter "is insecure and can't be made secure". Since .NET 9 the in-box implementation throws on use. Use System.Text.Json into plain types.

var dto = System.Text.Json.JsonSerializer.Deserialize<NoteDto>(body)
          ?? throw new InvalidOperationException("empty body");

For Json.NET, keep TypeNameHandling at None. Microsoft also lists SoapFormatter, NetDataContractSerializer, LosFormatter and ObjectStateFormatter as dangerous because they allow unrestricted polymorphic deserialization.

Node.js#

Use JSON.parse, then validate the shape before you use any field. A schema library keeps the allowed fields explicit.

import { z } from "zod";

const Note = z.object({ title: z.string().max(200), body: z.string() }).strict();
const note = Note.parse(JSON.parse(req.body));   // throws on extra or wrong fields

Do not revive functions or class names from input. Pair this with the allow-list ideas in our mass assignment guide.

Signing and its limits#

OWASP says to accept only signed data. The Python and PHP documentation both suggest hmac for data you store and read back. A signature proves who wrote the data, so it blocks an outside party who cannot get the key. It gives no protection if the key leaks, if an attacker can get the server to sign data, or if the signer itself was fed bad input. Keep the type allowlist even when you sign.

How does it differ from injection flaws?#

Injection, such as SQL injection, sends data that an interpreter reads as commands. Insecure deserialization sends data that the runtime reads as objects, and the harm comes from the classes the data selects. The fixes differ too. Parameterized queries stop injection. A safe data format and a type allowlist stop deserialization flaws. A single request can show both, so testers check each one separately.

What is the testing checklist?#

Use this list on each endpoint that accepts data from a client or from another system. Test only with written authorization.

  • Search code for the unsafe calls in the variants table. OWASP also suggests searching for readUnshared, readResolve, readExternal in Java and TypeNameHandling in .NET.
  • List every place where a serialized value crosses a trust boundary: cookies, hidden fields, headers, queues, caches, file uploads, import features.
  • Check each value for the signatures listed above.
  • Send an unchanged value and a changed value, and compare responses.
  • Confirm that signed data rejects a changed value and a missing signature.
  • Confirm the app rejects unexpected types and oversized input without a crash.
  • Check dependency versions against the OWASP library notes and the vendor advisories.
  • Record a harmless signal only when the scope allows it, and note the time and source.
  • Confirm the fix with a retest from the same session type.

When should you get a professional test?#

Get a test when your application reads serialized objects from clients, queues or files, or when you use frameworks that still ship native deserialization. A web application penetration test covers cookies, headers and uploads. An API penetration test covers JSON bodies, polymorphic types and message payloads. Excession Labs agrees the scope in writing, checks the evidence for each finding and writes the report.

Questions

What is the difference between serialization and deserialization?

Serialization turns an object and its fields into a flat format, such as bytes or text, that you can store or send. Deserialization reads that format and rebuilds the object. PortSwigger describes deserialization as restoring the byte stream to a functional replica of the original object.

What is a deserialization attack?

An attacker changes serialized data that the application will read back. Because the app may create objects of any class it can load, the data can steer the app into code paths the developer never meant to expose. The result can be denial of service, access control bypass or remote code execution.

How do I prevent insecure deserialization in Java?

Do not read untrusted data with ObjectInputStream if you can avoid it. Use JSON with DTOs instead. If you must use native serialization, set an ObjectInputFilter allowlist that ends with a reject-all rule and add limits for depth, references and bytes. OWASP warns that filtering alone does not make untrusted deserialization safe.

Is signing serialized data enough?

Signing stops an outside party from changing the data, as long as the key stays secret. It does not make the format safe if an attacker can get a valid signature, for example through a second flaw or a leaked key. Combine signing with a safe format and a type allowlist.

Does JSON have deserialization problems?

Plain JSON parsing into data types is safe from this class of flaw. Problems return when a JSON library lets the input choose the class to create. OWASP lists settings such as Json.NET TypeNameHandling and old fastjson autotype behavior as the risky cases.

Sources

  1. OWASP Deserialization Cheat Sheet (checked 9 November 2026)
  2. CWE-502: Deserialization of Untrusted Data (checked 9 November 2026)
  3. OWASP Top 10:2025 A08 Software or Data Integrity Failures (checked 9 November 2026)
  4. PortSwigger Web Security Academy: Insecure deserialization (checked 9 November 2026)
  5. Microsoft Learn: Deserialization risks in use of BinaryFormatter and related types (checked 9 November 2026)
  6. Python documentation: pickle (checked 9 November 2026)
  7. PHP manual: unserialize (checked 9 November 2026)
  8. Java SE 21 Core Libraries: Serialization Filtering (checked 9 November 2026)
  9. NVD: CVE-2015-4852 (Oracle WebLogic Server) (checked 9 November 2026)
  10. NVD: CVE-2017-9805 (Apache Struts REST plugin) (checked 9 November 2026)