Skip to content

Insecure deserialization

CWE-502OWASP A08:2021Updated August 31, 20266 min read

Insecure deserialization is a vulnerability where an application rebuilds objects from untrusted bytes, letting an attacker trigger a gadget chain that runs code. Native readers such as pickle, PHP unserialize and Java readObject are the usual culprits. The fix is a data-only format like JSON with a schema, plus signed and allowlisted payloads.

Applications constantly turn objects into bytes and back: a session stored in a cookie, a cached record, a message on a queue, an object passed between two services. Turning the bytes back into a live object is deserialization, and once those bytes come from someone you do not trust, that step can do far more than rebuild data. This article explains how insecure deserialization leads to remote code execution and how to design it away.

What is insecure deserialization?

Insecure deserialization is a vulnerability where an application rebuilds program objects from data it does not control, and the act of rebuilding runs attacker-chosen code or sets attacker-chosen state. Serialization writes an object out as a stream of bytes; deserialization reads that stream and reconstructs the object. The danger lies in how much a native deserializer is willing to do on your behalf while it puts the object back together.

An everyday comparison: a flat-pack cabinet ships with an instruction sheet. Normally the sheet tells the worker which panels to screw together. A native deserializer is a worker who follows any instruction on the sheet, so if a stranger slips in a line that reads “and then unlock the front door”, the worker does that too. The bytes are not only furniture parts, they are instructions, and some formats let those instructions call code.

The formats that allow this are well known: pickle in Python, unserialize in PHP, readObject in Java and BinaryFormatter in .NET. They rebuild whole objects rather than plain data, and they call methods automatically as they do so. The route to code execution is called a gadget chain. An attacker rarely finds a single method that runs a command outright. Instead they combine ordinary methods that already exist in the application and its libraries, methods that fire on their own while an object is reconstructed, until the chain ends at something dangerous such as a process call. Ready-made chains for common libraries ship in tools such as ysoserial for Java and phpggc for PHP, which is why an attacker often needs no research of their own.

How does an insecure deserialization attack work?

Take a service that keeps a little session state on the client. It base64-encodes a pickled dictionary into a parameter, then reads it back on the next request. The developer trusts that the value comes back unchanged.

Vulnerable:

import base64, pickle
from flask import Flask, request

app = Flask(__name__)

@app.route("/session/load")
def load_session():
    raw = base64.b64decode(request.args["state"])
    # pickle.loads rebuilds any Python object described in the bytes
    session = pickle.loads(raw)
    return f"Welcome back, {session['user']}"

A normal client sends a pickled dictionary and everything works. But the pickle format carries an opcode that calls a callable with arguments, exposed to any class through its __reduce__ method. The attacker writes a tiny class whose __reduce__ returns a function and the arguments to call it with, then serializes an instance of it:

import base64, os, pickle

class Payload:
    def __reduce__(self):
        return (os.system, ("id",))

print(base64.b64encode(pickle.dumps(Payload())).decode())

They drop the resulting string into the request:

GET /session/load?state=gASVJAAAAA... HTTP/1.1
Host: app.example.com

When pickle.loads reaches the reduce opcode, it calls os.system("id") before any of your own code runs. There is no semicolon to inject and no quote to escape: the format itself carries the instruction to execute. Java behaves the same way when readObject rebuilds a byte stream, and PHP the same when unserialize reconstructs an object and its magic methods fire.

The cure is not to filter the bytes but to change the format. Move to a data-only representation that describes values, not objects and not code. JSON parsed into plain dictionaries and lists cannot call a function while it parses. Then validate the result against a schema so the shape is what you expect, and if the payload has to survive a round trip through the client, sign it so the server accepts only data it produced.

Secure:

import base64, hashlib, hmac, json
from flask import Flask, request

app = Flask(__name__)
KEY = app.config["SESSION_KEY"]  # loaded from the environment, never hard-coded

def verify(state, tag):
    expected = hmac.new(KEY, state, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, tag):
        raise ValueError("bad signature")

@app.route("/session/load")
def load_session():
    state = base64.b64decode(request.args["state"])
    verify(state, request.args["sig"])       # reject anything we did not sign
    data = json.loads(state)                  # JSON builds only dict, list, str, number
    user = data.get("user")
    if not isinstance(user, str) or not user.isalnum():
        raise ValueError("bad user")
    return f"Welcome back, {user}"

Three defences stack here. JSON reconstructs only data, so there is no reduce opcode to abuse. The HMAC signature means a tampered or attacker-authored payload is rejected before it is ever parsed. The type and value checks reject a well-formed but unexpected document. The pickle payload from before now fails at the signature step, and even unsigned it could never reach os.system through json.loads.

JSON is not automatically safe. Libraries that embed class names in the document and instantiate them, such as Jackson with default typing switched on, .NET BinaryFormatter, or a serializer with TypeNameHandling set to All, bring the gadget-chain problem straight back on top of JSON. Keep type information out of the document and never let the parser choose which class to build from the input.

What is the impact of insecure deserialization?

The headline outcome is remote code execution (RCE): a working gadget chain runs commands with the privileges of the application process, which is about as bad as a bug gets. The range is wider than that, though. Depending on which classes are available, the same flaw can escalate to a privilege bypass by forging an object that marks the session as an administrator, a denial of service by deserializing a structure that exhausts memory or CPU, or data tampering and path traversal through objects that touch the filesystem.

That is why the severity spans high to critical. A payload that only crashes a worker is serious; one that lands a shell on a server with database access and open outbound traffic is critical. Because the dangerous gadgets usually live in third-party libraries, a codebase can be exploitable through a dependency the team never calls directly, which makes the flaw easy to overlook and hard to reason about from your own code alone.

How do you detect insecure deserialization?

Start by finding where serialized data crosses a trust boundary: cookies, hidden form fields, API parameters, message queues, cache entries and uploaded files. Learn the fingerprints. Java serialized objects begin with the hex bytes ac ed 00 05 and base64-encode to a string that starts with rO0. PHP serialized data shows the O: and a: prefixes. Python pickle carries its own opcodes and usually arrives base64-encoded. Any of these reaching a server from a client deserves a closer look.

In code, search for the sinks: pickle.loads, yaml.load without a safe loader, unserialize in PHP, readObject and ObjectInputStream in Java, and BinaryFormatter in .NET. To confirm that a sink is actually exploitable rather than merely present, testers build a payload from published gadget chains in ysoserial or phpggc for the exact libraries in use, often proving a blind case with a timing delay or an outbound DNS lookup to a domain they control. Scanners flag some patterns but rarely prove a chain end to end. AssistSec examines these paths during a penetration test and shows, per finding, which object triggered execution.

How do you prevent insecure deserialization?

  • Do not deserialize untrusted data with a native deserializer. pickle, unserialize, readObject and BinaryFormatter were never designed to be safe against hostile input.
  • Prefer a data-only format with a schema. JSON, Protocol Buffers or a similar format parsed into plain data cannot execute code while it parses; validate the result against a strict schema.
  • Sign payloads that round-trip through the client. An HMAC over the bytes lets the server accept only data it produced, so tampering is rejected before parsing begins.
  • Allowlist types when a native format is unavoidable. Restrict deserialization to an explicit set of expected classes, for example with a Java serialization filter, and refuse everything else.
  • Keep dependencies patched and minimal. Gadget chains live in library code, so removing unused dependencies and applying updates shrinks the set of gadgets an attacker can reach.
  • Run with least privilege and restrict outbound traffic. If a chain does fire, a confined process with no free egress makes the attacker’s next step considerably harder.

Sources

Frequently asked questions

What is a gadget chain in insecure deserialization?

A gadget chain is a sequence of ordinary methods, already present in the application and its libraries, that fire automatically while an object is rebuilt. The attacker links these methods together so the chain ends at something dangerous, such as a process call or a file write. Ready-made chains for popular libraries are published in tools like ysoserial for Java and phpggc for PHP, so the attacker often needs no original research.

Is JSON safe from insecure deserialization?

JSON parsed into plain data such as dictionaries, lists and strings cannot execute code while it parses, so it removes the classic gadget-chain risk. The catch is type-aware libraries that embed class names in the document and instantiate them, such as Jackson with default typing or a .NET serializer with TypeNameHandling enabled. Those reintroduce the same problem on top of JSON, so keep type information out of the document.

Why is Python pickle considered dangerous?

The pickle format includes an opcode that calls a callable with arguments, exposed through an object's __reduce__ method. An attacker who controls the bytes can therefore make pickle.loads run any function during reconstruction, before your own code sees the data. The Python documentation itself warns never to unpickle data from an untrusted source.

How do I fix insecure deserialization in Java?

Stop passing untrusted bytes to readObject and ObjectInputStream, and move the data to a schema-validated format such as JSON or Protocol Buffers. Where native serialization is unavoidable, use a serialization filter (JEP 290) to allowlist the exact classes you expect and reject everything else. Keep libraries patched, since gadget chains live in dependency code rather than your own.

Related articles

Press / to search · Esc