Insecure Deserialization

Insecure Deserialization: How a Serialized Blob Becomes Remote Code Execution

When an application rebuilds objects from untrusted serialized data, attacker-crafted gadget chains can run code during deserialization itself. Here is how object injection leads to RCE, and why data-only formats are the fix.

SelfSec Team3 min read
Part 1 of 5from theInsecure Deserializationseries
(os.system, ("id",))
On this page

Serialization turns an in-memory object into a stream of bytes you can store or send; deserialization rebuilds the object from those bytes. Insecure deserialization happens when an application reconstructs objects from data an attacker controls without checking its type or integrity. The danger is that deserialization is not a passive copy — for many formats, reconstructing the object runs real code. A carefully crafted blob can therefore turn into remote code execution the moment it is parsed.

This makes it one of the highest-severity bugs on the web: a single deserialized cookie or API field can hand an attacker the server. This post shows how a trusted serialized blob becomes object injection and RCE, and how to stop feeding untrusted data to a deserializer.

How it happens

The root cause is treating a serialized blob from a cookie, parameter or API body as if it were safe data. A common shape is a "remember me" or session feature that stores object state on the client and rehydrates it on the next request:

import pickle, base64

@app.route("/load")
def load():
    blob = base64.b64decode(request.cookies.get("state"))
    state = pickle.loads(blob)
    return f"Welcome back, {state.username}"

The developer assumes the cookie still holds the object the server wrote. But it is sent by the client, so the attacker can replace it with any serialized payload they like — and pickle.loads will reconstruct whatever that payload describes, executing the type's reconstruction logic in the process.

A concrete attack

Python's pickle invokes a method during reconstruction, which an attacker can repurpose to run a shell command. They build a malicious object, serialize it, and drop it into the state cookie:

import pickle, base64, os

class Exploit:
    def __reduce__(self):
        return (os.system, ("id > /tmp/pwned",))

payload = base64.b64encode(pickle.dumps(Exploit())).decode()

When the server calls pickle.loads on that cookie, it does not just create an object — it executes os.system("id > /tmp/pwned") on the host. The attacker has gone from "I can set a cookie" to "I run commands on your server."

The same idea spans ecosystems, using gadget chains — sequences of methods already present in the app's libraries that, wired together, reach a dangerous sink:

Java:    ObjectInputStream.readObject on an Apache Commons gadget chain
PHP:     unserialize() triggering __wakeup / __destruct magic methods
.NET:    BinaryFormatter on a known formatter gadget
fastjson: an @type / autoType directive that loads an attacker-named class

The bytes were never just data. For these formats, deserializing them is execution — the attacker is not exploiting a parser bug, they are using the deserializer exactly as designed, on input it should never have trusted.

What an attacker gains

Because the payload runs inside the application process, the impact is severe:

  • Remote code execution — arbitrary commands on the server, the most common outcome.
  • Authentication bypass and privilege escalation — forging objects that represent a logged-in or admin user.
  • Denial of service — crafted objects that exhaust memory or CPU on parse.
  • Full server compromise — RCE typically leads to data theft, lateral movement and persistence.

The fix: don't deserialize untrusted input; use data-only formats

The only fully reliable rule is to never deserialize data from an untrusted source with a format that can instantiate arbitrary types. Replace native serialization with a data-only format like JSON, parsed into a known schema with explicit fields:

import json

@app.route("/load")
def load():
    data = json.loads(request.cookies.get("state") or "{}")
    username = str(data.get("username", ""))
    if not username.isalnum():
        abort(400)
    return f"Welcome back, {username}"

JSON parsing produces plain strings, numbers and lists — never an arbitrary object whose construction runs code. Reinforce it with a few principles: keep client-held state minimal and sign it (e.g. an HMAC) so it cannot be tampered with; if a binary format is unavoidable, enforce a strict type allowlist so only expected classes can be instantiated and disable dangerous features like fastjson autoType or .NET BinaryFormatter entirely; and use yaml.safe_load rather than yaml.load. Treat every serialized blob crossing a trust boundary as hostile.

How SelfSec proves this one

Scanner behaviour
Module
Insecure Deserialization
How it probes
Detects serialized formats in parameters, cookies and bodies, then submits benign marker payloads for the runtime it identified.
How it confirms
An out-of-band interaction or a distinguishable parser state confirms the sink; a decoding difference on its own is not enough.

Reported as CWE-502 · ATT&CK T1190 · SARIF 2.1.0

A reflected response alone is not enough to promote a Insecure Deserialization finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about Insecure Deserialization

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The Insecure Deserialization series

  1. 01OverviewInsecure Deserialization: How a Serialized Blob Becomes Remote Code ExecutionReading
  2. 02.NET.NET Deserialization: BinaryFormatter, Json.NET and TypeNameHandling4 min
  3. 03JavaJava Deserialization: ObjectInputStream and the Gadget-Chain Problem4 min
  4. 04PHPPHP Object Injection: unserialize() and the Magic-Method Chain5 min
  5. 05PythonPython Deserialization: pickle, PyYAML and the __reduce__ Trap4 min

Related reading