Choose your stack

Node.js · Express

Fix insecure deserialization in Node.js and Express

Treat the token as data: JSON.parse it and rebuild only an allowlisted type - map $type to one known class, never the class the payload names.

Open the fix card
Python · Flask

Fix insecure deserialization in Python and Flask

Do not rebuild objects from the token. Decode it as JSON and copy only the fields you expect into a fixed shape; pickle.loads runs code chosen by the bytes.

Open the fix card
Java · Spring Boot

Fix insecure deserialization in Java and Spring Boot

Do not deserialize untrusted bytes with ObjectInputStream: its readObject runs code chosen by the payload.

Open the fix card
C# · ASP.NET Core

Fix insecure deserialization in C# and ASP.NET Core

Do not let the payload choose the type. With Newtonsoft.Json keep TypeNameHandling at None and bind to a concrete class; a SerializationBinder blocklist is not enough and even quiets the analyzer.

Open the fix card
Python · FastAPI

Fix insecure deserialization in Python and FastAPI

Do not rebuild objects from client data. Parse the token as JSON with a Pydantic model (Profile.model_validate_json(raw)), which keeps only declared fields of declared types; pickle.loads runs code chosen by the bytes.

Open the fix card
Ruby · Rails

Fix insecure deserialization in Ruby on Rails

Do not rebuild objects from the token: Base64-decode it and JSON.parse the bytes, then copy only the fields you expect into a fixed shape; Marshal.load runs code chosen by the bytes.

Open the fix card
PHP · Laravel

Fix insecure deserialization in PHP and Laravel

Do not rebuild objects from the token: base64-decode it and json_decode the bytes, then copy only the fields you expect; unserialize instantiates whatever class the string names and runs its __wakeup.

Open the fix card

Stacks without a card

How it happens

  • A format that embeds types or code is fed attacker data: Python pickle and PyYAML yaml.load build arbitrary objects and call arbitrary callables; Java ObjectInputStream runs each class's readObject; Newtonsoft.Json with TypeNameHandling reads a $type field and constructs it.
  • The payload names a class already on the server's classpath whose side effects form a 'gadget chain': a property setter opens a process, a readObject writes a file, a finalizer runs a command. The object never has to be the type the code expected.
  • In dynamic languages the object graph itself is the weapon. An unsafe recursive merge of a parsed JSON body copies a __proto__ or constructor.prototype key onto Object.prototype, so every object in the process gains the attacker's property (prototype pollution).
  • Hand-made fixes fail. Blocklisting known gadget class or type names only stops the samples you have seen: a new gadget, a different package, a case or whitespace variant, or constructor.prototype instead of __proto__ walks straight past the list.

Fix principles

  • Treat a serialized payload from a client as hostile data, not as an object. Parse it with a format that carries neither types nor code (JSON), into a fixed, concrete shape you declare.
  • Validate the parsed data against a schema before use: known fields, known types, known ranges. Reject anything else.
  • Never call pickle.loads, yaml.load (unsafe loaders), ObjectInputStream or TypeNameHandling.All/Auto on data a client can influence.
  • When typed or polymorphic deserialization is genuinely required, allowlist the exact set of types that may be created (ObjectInputFilter, [JsonDerivedType], a type map) and reject everything else; do not blocklist.
  • When merging a parsed object into your own, refuse the keys __proto__, constructor and prototype, or build onto a null-prototype object, or copy only an allowlist of known keys.
  • Prefer signing or MACing any state you must round-trip through a client so it cannot be swapped for a crafted payload, and keep deserialization error text off the wire.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong (blocklist) implementations must fail. run-all.mjs does this for every card.
  • The exploit tests send a payload whose only job is to fire a harmless marker (a file written during deserialization, or a polluted prototype) and require that the marker never fires. Functional tests round-trip the legitimate seed profile and preferences.
  • Static analysis is strong for the obvious sinks (pickle, yaml.load, ObjectInputStream, TypeNameHandling) and weak for hand-rolled type resolvers and unsafe merges; each card's scannerRules lists what was checked and what it found.

References

  1. OWASP Deserialization Cheat Sheet
  2. OWASP Top 10:2025 A08 Software or Data Integrity Failures
  3. CWE-502: Deserialization of Untrusted Data
  4. CAPEC-586: Object Injection
  5. OWASP Top 10:2017 A8 Insecure Deserialization (background)

Attack patterns:

Practice in the browser

Guides on this weakness