OWASP Top 10 Deep Dive · 6 of 8

Insecure Deserialization — Untrusted Data Becomes Code

Some serialization formats don't just store data — they store object graphs that, when read back, run constructors, setters, and class methods. Hand attacker-controlled bytes to one of those deserializers and you've handed over arbitrary code execution. The category includes Java native serialization, Python pickle, .NET BinaryFormatter, and any "magic-method" format whose deserialization triggers behavior.

RCEPickleJava SerializationBinaryFormatterGadgets
← Back to Security
Quick Facts

What's Actually Going On

Basic Concepts

  • Serialization: turning an object into bytes. Deserialization: turning bytes back into an object.
  • The dangerous formats execute code during deserialization — they invoke constructors, readObject hooks, setters, __reduce__, and other lifecycle methods. Crafted bytes can chain those into arbitrary calls.
  • Gadget chains: attackers don't write new code — they assemble sequences of existing classes in your dependencies whose deserialization side effects, in combination, run shell commands. Tools like ysoserial generate them automatically.
  • Severity: typically critical — full remote code execution as the application user.
  • JSON, Protobuf, Avro, YAML (safe loader): these are data formats. They don't trigger object methods on parse. They're safe with respect to this category — when used correctly.
The Family

Where This Bites by Stack

Java Native Serialization

The original. ObjectInputStream.readObject() on attacker bytes has been remote code execution since the early 2000s. Famous CVEs in Apache Commons Collections, Spring, JBoss — all gadget-chain attacks via Java's built-in serialization.

Fix: stop using Java native serialization for anything that crosses a trust boundary. Use JSON or Protobuf instead. If you absolutely can't, use ObjectInputFilter (Java 9+) to allow-list classes. Better still — don't.

Python pickle

Pickle is RCE by design — its __reduce__ protocol exists to let objects describe how to reconstruct themselves, including by calling arbitrary functions.

Rule: never pickle.loads anything you didn't produce yourself in a context you control. Use JSON for API payloads, Protobuf for service-to-service, and trust nothing pickled by clients.

.NET BinaryFormatter and Friends

BinaryFormatter is officially deprecated and dangerous. So is NetDataContractSerializer, SoapFormatter, and unrestricted JavaScriptSerializer with type info. Use System.Text.Json or Newtonsoft.Json with TypeNameHandling.None. Microsoft's own guidance is clear: don't use BinaryFormatter for anything other than internal trusted data — and ideally not at all.

YAML Default Loaders

PyYAML's yaml.load — the default before recent versions — instantiated arbitrary Python objects from tags like !!python/object/apply:os.system. Use yaml.safe_load always. Same lesson in Ruby's YAML.load (use safe_load) and the older Java SnakeYAML defaults.

JSON With Polymorphic Type Info

JSON is data — but libraries that embed type names so they can reconstruct subclasses (Jackson with default typing, Newtonsoft with TypeNameHandling=All, Gson with RuntimeTypeAdapterFactory) re-introduce the problem. Attacker tells the deserializer to instantiate any class on the classpath; gadget chains are back.

Fix: never enable open polymorphic typing on data from untrusted sources. If you must deserialize one of several types, use a tagged union with an explicit allow-list.

PHP unserialize

Same family. PHP magic methods (__wakeup, __destruct) trigger on deserialization. Use JSON. Avoid unserialize on anything untrusted.

Defenses

How to Stop It

1. Use Data Formats, Not Object Formats

JSON, Protobuf, Avro (with explicit schema), MessagePack, FlatBuffers, CBOR. These describe data — strings, numbers, arrays, maps. Deserializing them does not invoke arbitrary methods.

2. Don't Cross Trust Boundaries With Object Streams

Java native serialization and pickle are fine within a trust boundary — caching your own data, intra-process state. They become RCE when they touch the network, a queue, a cookie, an upload, or the database where untrusted input lives.

3. If You Must, Allow-List Classes

Java's ObjectInputFilter rejects classes not on a list. Jackson's PolymorphicTypeValidator does the same. The list should be tiny — ideally only your own DTOs. Anything broader and gadget chains will eventually find a way through.

4. Sign or HMAC Serialized Blobs

If a blob has to round-trip through an untrusted client (a session cookie, a queue), HMAC it with a server-side key. Reject anything whose signature doesn't verify. This stops attackers from fabricating new payloads — though it doesn't help if the secret leaks or the format itself is unsafe inside a trust boundary.

5. Keep Dependencies Slim

Gadget chains depend on classes already on your classpath. Every dependency you don't need is one fewer source of gadgets. Audit what's there with mvn dependency:tree, npm ls, pip list; remove what isn't pulling its weight.

6. Patch Promptly

Most disclosed deserialization CVEs come with new gadgets in popular libraries. Dependabot/Snyk alerts, automated minor-version updates, and a process for evaluating advisories are all part of the defense. Insecure deserialization is "low-frequency, high-severity" — when one lands in your stack, it matters.

Decision

The Quick Rule

Use JSON or Protobuf for anything that crosses a trust boundary. Reach for native object serialization only inside one process you fully control. If a feature seems to require deserializing complex objects from outside, redesign it — that's the bug, not the constraint.

Continue

Other OWASP Top 10