Insecure deserialisation turns a data format into a code execution path: when an application rebuilds objects from bytes an attacker controls, the attacker chooses which classes are created and which of their methods run. The weakness, CWE-502 Deserialization of Untrusted Data, sits in OWASP A08:2025 Software or Data Integrity Failures and keeps producing critical remote code execution flaws in Java, .NET, PHP and Python platforms, and now in machine learning model files. This guide goes deeper than the summary in the Web Application Attacks Guide: it explains object injection and gadget chains conceptually, shows where serialised data hides in each platform, and sets out the platform controls (JEP 290 filters, the end of BinaryFormatter, PHP allowed_classes, restricted unpicklers, Keras safe mode) plus detection, testing and rollout. It is for application security engineers, platform owners and developers, and it contains no payloads or exploitation steps.
Key takeaways
- Do not deserialise native objects from untrusted input. Data-only formats such as JSON bound to fixed types remove the class; filters, binders and allowlists only narrow it.
- The danger is in your dependencies, not your code. Gadget chains are built from ordinary classes already on the classpath, so a safe-looking endpoint is exploitable if any library supplies the pieces.
- Some serialisers cannot be fixed. Microsoft says BinaryFormatter "is insecure and can't be made secure" and removed it from .NET 9; Python, PHP and Java documentation give similar warnings for pickle, unserialize and unfiltered ObjectInputStream.
- Signing moves the risk to a key. HMAC-signed or MAC-protected state, such as ASP.NET ViewState, is only as safe as the key, and leaked machine keys have kept attackers inside after patching.
- Model files are serialised code. Pickle-based model formats and unsafe loader options execute code at load time, so model registries need the same controls as application input.
How deserialisation becomes object injection
The OWASP Deserialization Cheat Sheet defines serialisation as "turning some object into a data format that can be restored later" and deserialisation as rebuilding the object from that data. CWE-502 lists the alternate terms teams will meet in advisories: marshalling and unmarshalling, pickling and unpickling, and PHP object injection.
The difference between a safe and an unsafe deserialisation function is who decides the type. A JSON parser that fills a fixed data class only creates the types the code asked for. A native object deserialiser reads type names from the stream itself and instantiates them, then calls methods that the language runs automatically during reconstruction: Java readObject and related hooks, PHP __wakeup and __unserialize, Python reduction methods, .NET constructors and setters invoked by the formatter. That is object injection: the attacker supplies the object graph.
A gadget chain is a sequence of such automatically called methods, in classes that happen to be loadable by the application, that together perform something dangerous, such as reflection, file writes or process creation. No single class is a bug. CWE-502 notes that attackers may use "gadget chains to perform unauthorized actions" and warns that class allowlists help but "new gadgets are constantly being discovered". Three consequences follow:
- Remote code execution: the most cited outcome, and the reason deserialisation RCE flaws score critical.
- Denial of service: deeply nested or self-referencing graphs exhaust CPU and memory even when no gadget exists.
- Logic and access control tampering: modified fields in a serialised session, role or price object are trusted on return, which links CWE-502 to its peer CWE-915.
OWASP A08:2025 includes a scenario in which a front end sends serialised user state to Java microservices on every request; an attacker who recognises the Java serialisation signature achieves remote code execution. The category's rule is not to accept unsigned or unencrypted serialised data from untrusted clients without an integrity check.
Where serialised data hides
Inventory comes first, because most teams do not know where native deserialisation happens. The cheat sheet's identification markers are useful for traffic analysis and for searching logs and storage.
| Platform | Risky mechanism | How to recognise it | Safer default |
|---|---|---|---|
| Java | ObjectInputStream.readObject; RMI and JMX endpoints; XMLDecoder | Hex AC ED 00 05, Base64 beginning rO0, content type application/x-java-serialized-object | JSON bound to fixed classes; serialisation filters where native streams remain |
| Java JSON and XML libraries | Polymorphic typing that reads class names from the document; relaxed XML object mappers | Type or class properties in JSON; class names in XML elements | Disable polymorphic typing or allowlist base types |
| .NET | BinaryFormatter, SoapFormatter, LosFormatter, NetDataContractSerializer, ObjectStateFormatter | Base64 beginning AAEAAAD///// | System.Text.Json, XmlSerializer or DataContractSerializer with known types |
| .NET JSON | Json.NET with TypeNameHandling other than None; JavaScriptSerializer with a type resolver | A $type property in JSON | TypeNameHandling None, or a binder allowlist |
| ASP.NET Web Forms | ViewState in a hidden form field, protected by machine keys | __VIEWSTATE parameter | MAC validation on, unique generated keys, rotation |
| PHP | unserialize(); phar:// metadata on PHP before 8.0 | Serialised strings with object markers in cookies and parameters | json_decode(); allowed_classes set to false |
| Python | pickle, jsonpickle, YAML loaders that build arbitrary objects | Base64 beginning gASV for pickle | json; yaml.safe_load |
| Machine learning | Pickle-based model files; Keras loading with safe_mode=False | Model files from registries, hubs and uploads | Weights-only or data-only formats; safe loaders |
Look in cookies, hidden fields, queues, caches, session stores and uploads. Microsoft warns that payloads cross trust boundaries "without the developer realizing it", for example when a desktop application moves to a cloud service.
Java: ObjectInputStream, JEP 290 and serialisation filters
Java deserialisation is the reference case because the platform's native format is widely used for RMI, JMX, caching and session replication, and because large shared libraries supply gadgets. CWE-502 lists observed CVEs in a commonly used Java library from 2015. The defensive answer is layered.
- Remove native streams from untrusted paths: replace ObjectInputStream on network input with JSON or protocol buffers mapped to fixed classes. The cheat sheet says XMLDecoder makes it "close to impossible" to deserialise untrusted input safely.
- JEP 290 filters where native streams remain: JEP 290, "Filter Incoming Serialization Data", added the ObjectInputFilter interface, which returns ALLOWED, REJECTED or UNDECIDED for each class and receives graph metrics (array length, depth, references, stream bytes). A process-wide pattern filter is set with the jdk.serialFilter system or security property, without code changes; patterns are separated by semicolons, a leading "!" rejects, and limits such as maxdepth, maxrefs, maxbytes and maxarray reject oversized graphs.
- Turn filtering on deliberately: Oracle's documentation states that "A serialization filter is not enabled or configured by default." Write an allowlist of the application's own packages and the JDK types it needs, end the pattern by rejecting everything else, and set resource limits.
- Use context-specific filters: JEP 415 (JDK 17) added filter factories, set through ObjectInputFilter.Config.setSerialFilterFactory or a property, so different streams can have different allowlists instead of one compromise list for the whole JVM.
- Cover built-in endpoints: Oracle notes that the RMI registry, the RMI distributed garbage collector and JMX have built-in filters, and says "You should specify your own filters" for the RMI registry and DGC.
- Harden the classes you own: declare a final readObject that always throws on classes never meant to be deserialised, and mark sensitive fields transient so they cannot be set from the stream.
- Know the limits: the cheat sheet warns that "Filtering does not make arbitrary untrusted deserialization safe" and that filters are not called for strings and primitives, so bound input size separately. For third-party code you cannot change, a Java agent that hardens ObjectInputStream is the fallback.
As safe pseudocode: read each incoming class name, check it against a short allowlist before any object is constructed, enforce depth and size limits, and log rejections. Checking the type afterwards is too late; as the cheat sheet notes, "Execution may have already occurred."
.NET: BinaryFormatter, its siblings and ViewState
Microsoft's BinaryFormatter security guide is unambiguous: BinaryFormatter "is insecure and can't be made secure", Deserialize "is never safe when used with untrusted input", and calling it on a payload is "the equivalent of interpreting that payload as a standalone executable and launching it". Starting in .NET 9 the in-box implementation throws exceptions on use and the compatibility switches are gone.
- The same applies to siblings: SoapFormatter, LosFormatter, NetDataContractSerializer and ObjectStateFormatter "all perform unrestricted polymorphic deserialization and are dangerous, just like BinaryFormatter". .NET Framework 4.x applications still ship them.
- Binders are not a fix: Microsoft says this category of attack "cannot be mitigated with a SerializationBinder" or any configuration switch.
- Migrate to typed serialisers: System.Text.Json for JSON, XmlSerializer or DataContractSerializer for XML, deserialising into a known concrete type. Do not confuse DataContractSerializer with NetDataContractSerializer.
- JSON deserialisation attacks: JSON itself is not the problem; type metadata is. With Json.NET, keep TypeNameHandling at None, and if polymorphism is required use a custom binder that allowlists types.
ViewState deserialisation
ASP.NET Web Forms stores page state in a hidden field. The ValidationKey creates a MAC over it and the DecryptionKey optionally encrypts it, so the server trusts ViewState it can verify. If those machine keys leak, an attacker can produce ViewState the server accepts, and the server deserialises it. In February 2025 Microsoft reported more than 3,000 publicly disclosed machine keys that could be copied into applications unchanged, and a December 2024 attack that used one to load a post-exploitation framework. Its advice: never copy keys from public sources, generate unique keys and rotate them, encrypt the machineKey and connectionStrings sections of web.config, upgrade to ASP.NET 4.8 so the Antimalware Scan Interface (AMSI) can scan, and rebuild servers if a key was exploited, because rotation may not remove backdoors. The ToolShell SharePoint exploitation combined a deserialisation flaw (CVE-2025-49704) with theft of SharePoint's machine keys, which stayed valid until rotated.
PHP and Python
- PHP unserialize(): the manual warns "Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes", because object instantiation and autoloading can load and run code, and PHP calls __unserialize() or __wakeup() automatically. Use json_decode() for anything that crosses a trust boundary. Where unserialize() must stay for internal data, pass allowed_classes as false or a short list (omitting it allows every class), keep max_depth (default 4096) and verify an HMAC with hash_hmac() before parsing.
- Phar deserialisation: before PHP 8.0, ordinary file operations such as checking whether a phar:// path exists unserialised the archive's metadata, which enabled PHP framework vulnerabilities in 2018. The PHP 8.0 RFC limited this to explicit getMetadata() calls, which accept unserialize options such as allowed_classes. Applications on older PHP must reject the phar:// wrapper in any user-influenced path and should upgrade.
- Python pickle: the documentation opens with "The pickle module is not secure. Only unpickle data you trust", because malicious data can "execute arbitrary code during unpickling". It suggests json for untrusted data and HMAC signing against tampering. Where pickle cannot be removed from internal paths, subclass Unpickler and override find_class to allow a small set of globals and refuse the rest, as the documentation shows.
- YAML deserialisation: loaders that construct arbitrary Python objects behave like pickle. Use yaml.safe_load and do not register custom constructors that create arbitrary objects. CWE-502 lists a 2024 CVE in a data dashboard where YAML was the vector.
- jsonpickle: despite the name it restores arbitrary objects; review every jsonpickle.decode() call on external data.
Machine learning models and Keras safe mode
Many model formats wrap pickle, and some frameworks rebuild custom layers or lambda functions at load time. CWE-502 cites CVE-2024-37052, an ML model management platform flaw where a pickled model file was the vector.
- Keras safe mode: keras.saving.load_model has a safe_mode argument that disallows unsafe lambda deserialisation. It defaults to True and applies to the Keras v3 .keras format; the documentation warns that with safe_mode=False "loading an object has the potential to trigger arbitrary code execution". Never set it to False for a model you did not build, and block that pattern in code review.
- Prefer data-only weight formats: store and exchange weights in formats that cannot carry code, and use loader options that restrict loading to tensors where the framework offers them.
- Treat registries as supply chain: scan uploaded models for embedded code, pin model hashes, load untrusted models in an isolated, network-restricted sandbox, and record provenance. See the AI Supply Chain and AI-BOM Guide.
Choosing a defence
Use the lower rows only where the upper ones are impossible.
| Control | What it removes | Limits | Use when |
|---|---|---|---|
| Data-only format bound to fixed types | Attacker choice of type, and with it gadget chains | Migration effort; type metadata features must stay off | All new code and every untrusted input path |
| Integrity protection (HMAC, signature, ViewState MAC) | Tampering by clients who do not hold the key | Key theft or disclosure makes it worthless; does not help if the signer is untrusted | State round-tripped through clients, internal queues |
| Type allowlist (JEP 290 filter, find_class, allowed_classes, binder) | Most gadget classes | CWE-502: "alone is not a sufficient mitigation"; allowlists drift | Legacy native streams that cannot be removed yet |
| Isolation | Blast radius: the cheat sheet advises keeping gadget-bearing code away from internet-connected code | Does not stop the exploit | Model loading, document converters, legacy services |
| Runtime agent or RASP | Known dangerous classes at the deserialiser | Overhead; reject lists lag new gadgets | Third-party Java or .NET code you cannot change |
| WAF virtual patch | Commodity probes for known serialised markers | CWE-502 rates application firewalls moderate; encoded or signed payloads pass | A stopgap until a vendor fix lands |
- Small versus large organisations: a small team should ban native deserialisers with a lint rule and patch frameworks promptly. A large organisation needs a portfolio inventory of sinks, JVM-wide filters set centrally in base images, and a .NET Framework migration plan.
Detection, testing and measurement
- Find sinks in code: CWE-502 rates static analysis that traces data flow from sources to deserialisation sinks as high effectiveness. Flag ObjectInputStream, XMLDecoder, BinaryFormatter and siblings, TypeNameHandling values other than None, unserialize, pickle.load, yaml.load and unsafe model loading.
- Find serialised data in traffic: search proxy, WAF and gateway logs for the markers in the table above; an unexpected hit is both an inventory finding and a possible attack.
- Log filter decisions: enable Java serialisation filter logging and alert on REJECTED results in production.
- Watch the process tree: shells spawned by java, w3wp.exe or php-fpm, and outbound connections from application servers to unknown hosts, are the usual sign of deserialisation RCE. ATT&CK T1190 Exploit Public-Facing Application covers this, with detection strategy DET0080.
- Protect and watch keys: Microsoft suggests auditing read access to web.config and machine.config (Event ID 4663) and investigating access by anything other than the IIS application pool identity or w3wp.exe; Defender for Endpoint raises a "Publicly disclosed ASP.NET machine key" alert.
- Test safely: in authorised tests, confirm whether endpoints accept native formats and whether filters reject unexpected classes, using benign test classes; leave gadget exploitation to controlled lab environments.
Metrics:
- Native deserialisation sinks reachable from untrusted input, trending to zero.
- Share of JVMs running with a jdk.serialFilter allowlist and resource limits.
- Remaining .NET applications using BinaryFormatter or its siblings.
- Share of ViewState-enabled applications with unique, rotated machine keys.
- Share of model loads that use safe loaders and verified hashes.
Where identity fits
Signing and MAC keys are machine credentials. A ViewState ValidationKey, an HMAC key protecting a serialised cookie or a signing key for queued jobs lets whoever holds it make the application trust arbitrary objects. Store them in a secrets manager or key vault rather than in web.config or source, give each application its own key, rotate on a schedule and after any compromise, and alert on reads by unexpected identities. After a deserialisation incident, rotate every credential the process could read: machine keys, connection strings, service account passwords and cloud tokens. See the Secrets Management Guide and the Cryptographic Key Management Guide.
Implementation roadmap
- Inventory: find sinks and serialised markers; record owner, input source and format.
- Close untrusted paths: replace native deserialisation on internet-facing inputs with formats bound to fixed types.
- Filter what remains, in audit first: deploy a JVM-wide jdk.serialFilter allowlist with logging in a test ring, review rejections for two to four weeks, then enforce. Roll back by reverting the property in the base image.
- Retire .NET formatters: migrate off BinaryFormatter and siblings before moving to .NET 9 or later, where BinaryFormatter throws.
- Fix keys: regenerate ViewState machine keys that are default, shared or published, encrypt configuration sections and enable AMSI.
- Constrain PHP and Python: ban unserialize on external data, require yaml.safe_load, block pickle on external data with a lint rule and default model loaders to safe mode.
- Gate the pipeline: fail builds on new sinks and accept releases against ASVS 5.0 requirement 1.5.2 (object type allowlists, no mechanisms documented as insecure for untrusted input).
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| OWASP Top 10:2025 | A08:2025 Software or Data Integrity Failures | CWE-502 is a notable CWE; reject unsigned serialised data from untrusted clients |
| OWASP ASVS 5.0 | V1.5.2 Safe Deserialization | Allowlist object types; never use insecure mechanisms with untrusted input |
| MITRE CWE and CAPEC | CWE-502 (child of CWE-913, peer of CWE-915); CAPEC-586 Object Injection | Weakness and attack pattern tagging |
| NIST SP 800-53 Rev. 5 | SI-10, SI-7, SA-11 | Input validation, software and information integrity, developer testing |
| CIS Controls v8.1 | 16.1, 16.11, 16.12 | Secure development process, vetted modules, code-level checks |
| ISO/IEC 27001:2022 Annex A | A.8.28, A.8.29 | Secure coding, security testing |
| MITRE ATT&CK v20 | T1190; M1048 Application Isolation and Sandboxing; M1050 Exploit Protection | Initial access through deserialisation RCE and the matching mitigations |
Common failure patterns
- Reject lists instead of allowlists: blocking known gadget classes misses next month's gadget.
- Type checks after deserialisation: the dangerous methods already ran.
- Public machine keys: keys copied from public sources make MAC protection meaningless.
- Patching without rotating: after ToolShell-style intrusions, stolen keys outlive the patch.
- "Internal only" streams: RMI, JMX and queue consumers become reachable through SSRF or a compromised neighbour.
- Disabling safe loaders to make a model load: safe_mode=False added to fix one error becomes the default path.
Practitioner checklist
- Inventory every deserialisation sink and every serialised format crossing a trust boundary.
- Use data-only formats bound to fixed types for all untrusted input; keep type metadata features off.
- Set a jdk.serialFilter allowlist with resource limits on every JVM that still reads native streams, and log rejections.
- Remove BinaryFormatter, SoapFormatter, LosFormatter, NetDataContractSerializer and ObjectStateFormatter from .NET code.
- Generate unique ViewState machine keys, store them encrypted, rotate them and rebuild servers after exploitation.
- Ban unserialize, pickle and unsafe YAML loaders on external data, and upgrade PHP to 8.0 or later.
- Load models only with safe loaders, verified hashes and isolation; never set Keras safe_mode to False on untrusted files.
- Alert on shells spawned by application server processes and on filter rejections.
- Rotate every key and credential an exploited process could read.
Standards and references
- MITRE: CWE-502 Deserialization of Untrusted Data
- OWASP: A08:2025 Software or Data Integrity Failures
- OWASP Cheat Sheet Series: Deserialization Cheat Sheet
- OpenJDK: JEP 290 Filter Incoming Serialization Data
- Microsoft Learn: Deserialization risks in use of BinaryFormatter and related types
- Python Software Foundation: pickle, Python object serialization
- PHP Manual: unserialize
- Microsoft Security Blog: Code injection attacks using publicly disclosed ASP.NET machine keys
Related NHI Mgmt Group resources: Web Application Attacks Guide · Secure Coding Guide · Secrets Management Guide · AI Supply Chain and AI-BOM Guide · Zero-Day and Exploited Vulnerabilities Guide