Insecure deserialization: test what your app unpacks
Insecure deserialization is one of the quieter risks in a web app, and it hides in places you rarely look. Your app takes bytes from somewhere outside, like a cookie or a message queue, and turns them back into objects in memory. If those bytes came from a user and your code trusts them, an attacker can hand you a crafted blob that your app rebuilds into something you never meant to create. This post shows you where that happens, how to spot it, how to test it without wrecking your environment, and how to fix it.
What insecure deserialization is and why it matters
Serialization is turning an object into a stream of bytes so you can store it or send it. Deserialization is the reverse: reading those bytes and rebuilding the object. The problem starts when the bytes come from someone you do not control.
Some serialization formats do more than carry data. They carry type information and instructions about how to rebuild the object. When your app reads that, it may create arbitrary types, call methods, or set private fields, all from input a stranger sent. That is the gap. The format was designed to trust its source, and you pointed it at an untrusted source.
The OWASP Deserialization Cheat Sheet says attacks against deserializers have allowed denial of service, access control and remote code execution attacks. It is not loud. There is no obvious injection point in a URL. You have to know where your app unpacks data and whether that data can be forged.

What to search for, and how OWASP says to fix it. Simplified from the OWASP Deserialization Cheat Sheet.
Where your app unpacks untrusted data: cookies, tokens, queues, and caches
Start by listing every place bytes enter your app and get turned into objects. The usual suspects:
- Cookies and session state. Some frameworks store the whole session object in the cookie, serialized, instead of just a session ID. The client holds the bytes and sends them back.
- Tokens and view state. Hidden form fields or headers that carry a packed object between requests.
- Message queues. A producer serializes a job, a worker deserializes it. If anything untrusted can push to that queue, the worker is exposed.
- Caches. Objects cached to Redis or Memcached and read back later. If the cache can be poisoned, the read becomes a deserialization sink.
- File uploads and imports. Config files, saved game state, exported settings, anything a user uploads that your code loads as objects.
- Inter-service calls. One service sends a serialized payload to another over the network.
The dangerous combination is simple: untrusted bytes plus a deserializer that can build rich objects. Find both together and you have found a test target.
How to spot serialized data in requests (Java, PHP, Python pickle, .NET, YAML)
You can often recognize serialized blobs by their shape. Learn these markers and grep your traffic for them.
- Java. OWASP's markers:
AC ED 00 05in hex,rO0in Base64, or a response type ofapplication/x-java-serialized-object. In code, look forObjectInputStream.readObjectandXMLDecoder. - PHP. PHP's
serialize()output looks likeO:8:"UserName":2:{s:4:"name";...}. TheO:prefix means an object with a class name and field count. - Python pickle. Pickle data is binary and hard to eyeball, so search the code: any
pickle.loadorpickle.loadson external input is a red flag. OWASP is blunt: unpickling can execute arbitrary code, so never unpickle untrusted data.jsonpickle.decodeon input belongs on the same list. - .NET. OWASP notes
BinaryFormatteris dangerous and cannot be secured; also search forTypeNameHandlingset to anything other than None, and any serializer whose type comes from user input. - YAML. YAML looks friendly, but some loaders build arbitrary objects from tags like
!!python/object. If your code uses an unsafe YAML load on user input, treat it like pickle.
Walk through a request in your proxy. For each parameter, cookie, and header, ask: is this base64 or binary, does it decode to one of these shapes, and does it come back to the server to be unpacked? That inventory is the start of your test plan.
How to test deserialization safely without breaking production
Deserialization testing is risky because a working payload can run code or corrupt state. Keep it off production.
Set up a copy of the app in a staging or local environment with the same framework versions and libraries. Versions matter here because exploit chains depend on which classes are present. Turn on verbose logging so you can see what the deserializer does.
Start non-destructive. Take a known-good serialized blob from your own session and change one field. For a PHP object, flip a value inside the serialized string and watch whether the app accepts the altered object. For a session cookie, change a role or user ID field and see if the change sticks. You are testing whether the app trusts the structure, not yet whether you can run code.
Here is a worked example. Say your session cookie decodes to a PHP object:
O:4:"User":2:{s:4:"name";s:5:"alice";s:7:"isAdmin";b:0;}
Change b:0 to b:1, re-encode, and send it back. If the app now treats you as an admin, you have found insecure deserialization that trusts client-held state. You proved the weakness without any code execution payload.
Only escalate to object-injection or gadget-chain testing in an isolated environment you fully control, with a plan to restore state. Never fire an untested payload at a running queue or cache that real users share.
Common attack outcomes: tampered objects, privilege changes, and remote code execution
The results fall on a ladder, lowest to highest impact:
- Tampered objects. The attacker edits fields in a serialized blob the app trusts. Prices, quantities, flags, anything in the object becomes editable.
- Privilege changes. The example above. A boolean or a role string flips and the attacker gains access they should not have. This overlaps with authorization bugs, so pair this testing with your JWT and token checks.
- Object injection. The attacker makes the app build objects of types it did not expect. Those objects may run code in their constructors or magic methods during deserialization, before any of your logic runs.
- Remote code execution. The worst case. A chain of existing classes, a gadget chain, gets triggered during deserialization and ends in a command running on your server.
How to fix it: prefer plain data formats, sign payloads, and restrict allowed types
Fixes come in layers. Apply as many as you can.
Prefer plain data formats. The strongest fix is to stop deserializing rich objects from untrusted sources. Use JSON for data interchange and map it into your own types by hand, field by field. JSON carries values, not type instructions, so there is no built-in way to make it instantiate arbitrary classes. Avoid native serializers like PHP's unserialize, Python's pickle, Java native serialization, and .NET BinaryFormatter on anything a user can touch.
Sign payloads you must keep. If you genuinely need to round-trip state through the client, attach a server-side signature (an HMAC) and verify it before you deserialize. If the signature does not match, drop the request and never unpack the bytes. Keep the signing key on the server only.
Restrict allowed types. Never let the data choose the object type. Where your platform supports it, lock deserialization to an allowlist of safe classes; for Java, OWASP recommends serialization filters with a class allow-list and resource limits. If a payload tries to build a type that is not on the list, reject it. Use safe loaders: yaml.safe_load instead of the unsafe variant, JSON serializers instead of pickle.
Move state server-side. Store the object in your own session store and give the client only an opaque ID. Then there is nothing to tamper with. This is close to how you close mass assignment gaps: control exactly which fields the outside world can influence.
After you change code, retest. A fix that moves the deserializer or swaps a library can open a new sink. Make deserialization a line item in your review, the same way you would check template injection on pages built from user input or what your GraphQL API lets clients ask.
Frequently asked questions
Is JSON safe from insecure deserialization?
JSON itself carries only data, not type instructions, so a standard JSON parser will not instantiate arbitrary classes. The risk returns if you use a library feature that reads type hints from the JSON and builds objects from them, so keep parsing to plain values and map them into your types yourself.
How is insecure deserialization different from mass assignment?
Mass assignment is about an object getting fields set from input that should not be writable, usually through normal parsing. Insecure deserialization is about rebuilding whole objects, including their type, from attacker-controlled bytes, which can reach code execution rather than just field tampering.
Can a signed cookie or JWT still be deserialized insecurely?
Yes. A valid signature proves the payload was not changed, but if you then feed the trusted payload into an unsafe deserializer you still rebuild rich objects from it. Verify the signature first and then deserialize into plain data, never into arbitrary types.
Get started
Search your code today for the calls in the diagram, and run the one-field tamper test on the first cookie or job payload you find.

whitehatstoic builds software, tests web apps and AI systems for weak spots, and researches AI safety: "Build it right. Test it thoroughly. Keep it safe." Security testing covers web app and API review, prompt injection tests, and a retest after fixes, with a written report and clear fixes. Deserialization checks fit straight into that review alongside your sign-in and payment paths.
No single pass finds every weakness. Testing is scoped after a short call: book a meeting about security testing.
Building something that has to be safe? Book a meeting with whitehatstoic: tell us the product, the deadline and your biggest worry, and we reply with a plan and a price.
Comments
No comments yet.
Sign in or make an account to comment.