Insecure Deserialization: From Serialized Blob to Remote Code Execution
Preventing Insecure Deserialization
How to prevent insecure deserialization: do not deserialize untrusted data, prefer JSON with no polymorphic typing, allow-list classes, and patch gadgets.
Published
Insecure deserialization has one real fix and several supporting controls. The real fix is to not deserialize untrusted data in a dangerous format. Everything else reduces the chance a slip becomes RCE. This page is the defensive reference for the cluster. Want to see why the weak controls fail? Practice the bypasses on a live target.
The one control that matters: do not deserialize untrusted data
Native deserializers (PHP unserialize, Java ObjectInputStream, Python pickle, .NET BinaryFormatter) rebuild attacker-chosen types and run their lifecycle code. On data that crosses a trust boundary, do not use them at all. This single choice removes the entire vulnerability class, because there is no sink for a gadget to reach.
Prefer simple formats with no polymorphic typing
Switch client-facing and cross-service data to a format that encodes values, not types, and parse it into fixed structures:
Use JSON or Protobuf parsed into known types
Disable Jackson default typing, JSON.NET TypeNameHandling (keep it None), fastjson autotype
Avoid yaml.load/unsafe_load (use yaml.safe_load), pickle on external inputThe danger with JSON is never the format, it is a library configured to instantiate a class named in the data. Keep type handling off.
Integrity-protect what you must round-trip
If you must hand serialized state to a client and take it back (a cookie, a hidden field), make it unforgeable:
- Sign with an HMAC and verify the signature before parsing, so a tampered or forged blob is rejected before the deserializer runs.
- Or encrypt it so the client cannot author object graphs at all.
- Keep the key secret and rotate it if it may have leaked. A leaked key (the ASP.NET machine-key problem) reopens the attack.
Allow-list permitted classes
Where you cannot drop the native serializer, constrain what it can rebuild:
- Java:
ObjectInputFilter(JEP 290) rejects classes outside an expected set before instantiation. - PHP:
unserialize($data, ['allowed_classes' => false])blocks object instantiation entirely, or pass a specific list. - General: a resolver that only maps approved type names.
An allow-list breaks gadget chains because the library classes they need never get rebuilt.
Patch and trim gadget-prone libraries
Gadget chains live in libraries. Keep Commons Collections, Spring, and the rest patched, and remove unused libraries from the classpath so their gadgets are simply not present. Fewer libraries means fewer chains.
Defense in depth
Assume something slips and contain it:
- Least privilege for the process, so RCE lands with minimal rights.
- Isolation (containers, seccomp) so a successful exploit reaches little.
- Monitoring for deserialization errors and unexpected outbound connections, which catch probing and callbacks.
The layered picture
- Primary: do not deserialize untrusted data in a dangerous format.
- Supporting: simple formats with no type handling, integrity protection verified before parse, class allow-lists, patched and trimmed libraries, least privilege and isolation.
With the primary control in place, the rest is depth. Without it, the rest only raises the bar.
For the attack side of each of these, work back through the cluster from the pillar, and keep the cheat sheet handy. Ready to prove the weak controls fall? Sign up free and try it.
← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution