Back to Glossary
high severity · Vulnerability Glossary

Insecure Deserialization

Updated August 24, 2026

4 min read

insecure deserialization
deserialization vulnerability
gadget chain
ysoserial
php unserialize vulnerability
python pickle security
java readobject rce
objectinputfilter
owasp a08

Insecure deserialization happens when an application rebuilds objects from attacker-controlled bytes. Reconstruction runs real code — constructors, magic methods, custom readObject handlers — so it escalates to remote code execution more often than most injection classes. We rate it high, critical once a working gadget chain is proven: the outcome is code execution, not disclosure.

What is insecure deserialization?

Serialization flattens an object graph into bytes; deserialization rebuilds it. Native formats carry type information, so the bytes decide which classes the runtime constructs — control them and you control part of what runs.

The dangerous sinks:

  • JavaObjectInputStream.readObject(), plus anything wrapping it (RMI, JMX, JSF view state).
  • PHPunserialize(), which fires magic methods such as __wakeup(), __destruct() and __toString().
  • Pythonpickle.loads(); the docs warn that pickle is not secure against maliciously constructed data.
  • RubyMarshal.load().
  • .NETBinaryFormatter: obsoleted in .NET 5 (SYSLIB0011), disabled by default in .NET 8, removed in .NET 9 — the in-box implementation now throws.

JSON parsing is not this bug. JSON.parse(), json.loads() and System.Text.Json on defaults produce data, not types. The exception is polymorphic type resolution — Jackson default typing, Newtonsoft's TypeNameHandling.All — which lets the document name the class to instantiate.

It sits under OWASP Software or Data Integrity Failures (A08:2025, formerly A08:2021), having been its own category in the 2017 OWASP Top 10.

Why it's a risk / how it's exploited

The attacker supplies no code. They supply an object graph whose methods, as the runtime unwinds it, chain into something dangerous — a gadget chain built from classes already on your classpath. ysoserial (Java) and phpggc (PHP) catalogue those chains for Commons Collections, Spring, Laravel, Symfony and Monolog.

A realistic path: an app keeps session state in a cookie as a base64 ObjectInputStream blob. The attacker generates a CommonsCollections6 payload — a LazyMap/InvokerTransformer chain ending in Runtime.exec() — and swaps the cookie. It is deserialized in the servlet filter chain, above the authentication check, so the command runs before login state is evaluated.

Preconditions, plainly: you need a reachable sink fed by attacker-controlled bytes and a usable gadget in the installed dependencies. Neither is guaranteed, which is why the baseline is high rather than automatically critical — but when both line up you get code execution, far above informational findings like server version disclosure.

How to detect it

Hard to spot from outside: no header to check, no error to grep. Black-box detection means finding serialization markers in cookies, hidden fields and parameters.

# Java streams start AC ED 00 05 — base64 prefix "rO0AB"
curl -sI https://target.example/app | grep -i set-cookie | grep -oE 'rO0AB[A-Za-z0-9+/=]*'

# PHP serialized objects reflected in a response
curl -s https://target.example/page | grep -oE 'O:[0-9]+:"[A-Za-z0-9_\\]+"'

Worth investigating: aced 0005 at the head of a base64-decoded cookie, or O:8:"stdClass":1:{...} in a parameter. Not this bug: a JWT or an opaque session ID.

The dependable signals are internal, making this a SAST and dependency-scanning problem as much as a DAST one: grep for readObject, unserialize(, pickle.loads and Marshal.load, and audit dependencies for gadget libraries.

How to fix it

1. Don't deserialize untrusted input. Keep state server-side behind an opaque ID, or move to a data-only format with no type metadata.

2. If a blob must round-trip through the client, authenticate it before parsing:

import hmac, hashlib, json
mac = hmac.new(key, blob, hashlib.sha256).hexdigest()
if not hmac.compare_digest(mac, sig):
    raise ValueError("tampered")
data = json.loads(blob)      # data only — never pickle.loads

3. Allowlist classes. Java 9+ (backported to 8u121) ships ObjectInputFilter:

ObjectInputFilter f = ObjectInputFilter.Config.createFilter(
    "com.example.dto.*;java.base/*;!*");   // allow these, reject the rest
ois.setObjectInputFilter(f);

Patterns are semicolon-separated, a module/ prefix scopes one to a single module, and the trailing !* rejects the rest; -Djdk.serialFilter=... sets a JVM-wide default. In PHP 7.0+, unserialize($data, ['allowed_classes' => false]) returns inert __PHP_Incomplete_Class stubs, not live objects.

4. Kill polymorphic typing. Jackson's enableDefaultTyping() has been deprecated since 2.10 and is gone in 3.x; for genuine subtypes use @JsonTypeInfo with an explicit @JsonSubTypes list, or activateDefaultTyping() with a BasicPolymorphicTypeValidator allowlist. Leave Newtonsoft at TypeNameHandling.None.

Caveat: there is no edge configuration for this. A WAF rule matching rO0AB stops only the laziest payloads — blobs can be gzipped or hit a non-obvious sink — and a filter as broad as java.* still admits gadgets. It is code work, confirmed by web application security testing.

FAQ

Is JSON safe from insecure deserialization? Plain JSON parsing is safe: it yields maps, lists, strings and numbers, nothing else. It turns unsafe only when polymorphic type resolution is on, because the document then names the class to instantiate.

Can a scanner find insecure deserialization? Partly. Scanning reliably surfaces the signals — serialization markers in cookies and parameters, gadget-bearing library versions. Proving exploitability needs a working gadget chain against your dependencies — agent-driven testing, not signature matching.


Deserialization sinks hide in cookies and hidden fields no header check reveals. Run a free website vulnerability scan and let Exploita's AI pentest agent hunt serialized blobs across your attack surface, with a verified proof of concept.