An app stores a user's session or preferences by serializing an object and handing it back to the client, then trusts it on the next request:
import pickle, base64
# On response: cookie = base64(pickle.dumps(session_obj))
# On request:
session = pickle.loads(base64.b64decode(request.cookies["session"]))The problem: pickle.loads doesn't just read data — it reconstructs objects, and a pickle stream can specify which objects to build and how. An attacker replaces the cookie with a crafted payload whose reconstruction calls os.system, and your server runs their command the instant it loads the session — before any of your code inspects a single field. This is insecure deserialization, and its defining trait is that the damage happens during loading, so validating the result afterward is already too late.
Why "just loading data" runs code
Native serialization formats are designed to round-trip rich objects, so they encode type and construction, not only values. Reconstructing them invokes real code paths:
- Python
pickleexecutes reduce methods duringloads— arbitrary callables with arbitrary arguments. - Java
ObjectInputStreamcallsreadObjecton the deserialized classes; chains through libraries like Commons-Collections reachRuntime.exec. - PHP
unserializetriggers magic methods (__wakeup,__destruct) on reconstructed objects — the basis of "POP chains." - Ruby
Marshal.loadand .NETBinaryFormatterhave the same shape.
The attacker rarely injects new code. They assemble a gadget chain out of methods already present in your dependencies, so deserializing their object walks those methods to a dangerous sink. That's why the fix isn't "sanitize the object" — by the time you could, the code has already run.
Where it hides
Look for any place your app deserializes data that a user can influence:
- Session tokens, cookies, or "state" blobs stored client-side in a native format
- Message-queue payloads, cache entries (a serialized object in Redis/Memcached)
- File uploads processed by a library that deserializes (some document/model formats)
- APIs accepting
application/x-java-serialized-object, or JSON with polymorphic type hints (@class,$type) pickle/Marshal/unserializeanywhere near request-controlled bytes
The fix: use a data-only format, verify integrity, and don't resolve types
Three ideas, in priority order.
The principles
- Serialize data, not objects. Use JSON/Protobuf/MessagePack for anything crossing a trust boundary. They carry values, so loading them can't run code.
- Turn off type resolution. Never enable polymorphic/default typing on untrusted input (Jackson default typing,
$type/@classhints). If you truly need it, use a strict allowlist of permitted types. - Sign what the client holds. If a token or blob round-trips through the browser, HMAC-sign it and verify before parsing — better yet, keep session state on the server and hand out an opaque ID.
- Never
pickle/Marshal/unserializeuntrusted bytes. There is no safe way to deserialize an arbitrary native object; the reconstruction is the exploit.
One line to keep: deserialization runs before validation, so the only safe input is a format that can't express code. Choose the format, and the gadget chains have nothing to grab.
Walk the attack in the insecure deserialization simulation: swap a serialized session for a gadget payload, trigger execution on load, then move to signed JSON and watch it become inert. Pairs naturally with the API token storage guide — both are about not trusting state the client hands back.
Frequently Asked Questions
Related posts
Race Conditions in Web Apps: When Two Requests Beat Your Check
Check-then-act logic that looks correct on paper breaks when two requests run at once. Here's how TOCTOU race conditions drain balances and reuse one-time codes, and how atomic operations and locks fix them.
Aug 11, 2026 · 7 min readMass Assignment: How role=admin Ends Up in Your Update
Mass assignment lets an attacker set fields you never meant to expose — like isAdmin or accountBalance — by adding them to a request body. Here's why binding whole objects is dangerous and how to allowlist fields in Node, Rails, and Django.
Aug 10, 2026 · 6 min readWebSocket Security: Why the Same-Origin Rules Don't Apply
WebSockets skip the protections you rely on for normal requests — CORS doesn't apply and cookies are sent automatically, enabling cross-site hijacking. Here's how to validate origins, authenticate connections, and secure real-time apps.
Aug 2, 2026 · 6 min read