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

Source: https://penetrationtestservices.com/vulnerabilities/insecure-deserialization/
Publisher: Excession Labs (penetrationtestservices.com)
Last updated: 2026-11-09

Insecure deserialization explained: serialization vs deserialization, gadget chains, real CVEs, safe testing, and fixes for Java, Python, PHP, C# and Node.js.

> **Short answer:** 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:
> 
> ```http
> 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](/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.

```java
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.

```python
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`".

```php
$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.

```csharp
var dto = System.Text.Json.JsonSerializer.Deserialize(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.

```js
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](/vulnerabilities/mass-assignment/).

### 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](/vulnerabilities/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](/web-application-penetration-testing/) covers cookies, headers and uploads. An [API penetration test](/api-penetration-testing/) 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

- [OWASP Deserialization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html)
- [CWE-502: Deserialization of Untrusted Data](https://cwe.mitre.org/data/definitions/502.html)
- [OWASP Top 10:2025 A08 Software or Data Integrity Failures](https://top10.owasp.org/2025/A08_2025-Software_or_Data_Integrity_Failures/)
- [PortSwigger Web Security Academy: Insecure deserialization](https://portswigger.net/web-security/deserialization)
- [Microsoft Learn: Deserialization risks in use of BinaryFormatter and related types](https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide)
- [Python documentation: pickle](https://docs.python.org/3/library/pickle.html)
- [PHP manual: unserialize](https://www.php.net/manual/en/function.unserialize.php)
- [Java SE 21 Core Libraries: Serialization Filtering](https://docs.oracle.com/en/java/javase/21/core/serialization-filtering1.html)
- [NVD: CVE-2015-4852 (Oracle WebLogic Server)](https://nvd.nist.gov/vuln/detail/CVE-2015-4852)
- [NVD: CVE-2017-9805 (Apache Struts REST plugin)](https://nvd.nist.gov/vuln/detail/CVE-2017-9805)
