Insecure Deserialization: Risks and Remediation
How untrusted pickle, Java serialization, and PHP unserialize data can trigger unsafe behavior—and how to replace or constrain it.
💡 Plain English Explainer (ELI5)
Serialization is like mailing a set of assembly instructions with a model. Deserialization follows those instructions to rebuild data. Some formats can also name classes or invoke hooks while rebuilding objects; accepting untrusted instructions can therefore trigger behavior the receiver did not intend. The exact impact depends on the format, runtime, reachable classes, and application context.
Core Concepts & Key Terms
- Serialization / deserialization
- Converting data or objects to a stored or transmitted representation, then parsing or reconstructing them in a program.
- Deserialization hooks
- Runtime callbacks such as Python __reduce__, Java readObject, or PHP __wakeup / __unserialize that may run as objects are reconstructed.
- Gadget chain
- A sequence of already-present library behaviors that can be composed to produce an unintended effect when deserialization reaches those classes. Availability is application- and version-specific.
- Deserialization filter
- A policy that constrains classes and/or graph metrics while reading an object stream. It reduces risk but does not make untrusted native object streams a preferred data format.
How Unsafe Deserialization Can Become an Exploit Chain
Untrusted bytes reach an object decoder
A cookie, API body, queue message, or stored record contains data an attacker can influence.
The application invokes a native object format
The code passes those bytes to a decoder such as Python pickle, Java ObjectInputStream, or PHP unserialize().
Reconstruction invokes format-specific behavior
Depending on the format and reachable classes, constructors, hooks, autoloading, or library methods may run before application-level validation.
Impact depends on reachable behavior
A usable gadget or hook may cause unauthorized effects; other inputs may only fail or consume resources. Code execution is not automatic for every format or application.
Python / Flask: Unsafe Object Loading vs. Primitive JSON Validation
# VULNERABLE: pickle can execute behavior during unpickling.
from flask import Flask, request
import pickle
app = Flask(__name__)
@app.post("/api/session")
def load_session():
# Never deserialize attacker-controlled bytes with pickle.
session = pickle.loads(request.get_data())
return {"user_id": session.user_id}
# SAFER: parse JSON primitives, then validate expected shape.
from flask import Flask, abort, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024
@app.post("/api/session")
def load_session():
payload = request.get_json()
if not isinstance(payload, dict):
abort(400, "Expected a JSON object")
user_id = payload.get("user_id")
if type(user_id) is not int or user_id <= 0:
abort(400, "Invalid user_id")
# Resolve authorization and session state server-side.
return {"user_id": user_id}
Engineering Hardening Checklist
JSON still needs input-size limits and structural validation. For legacy object formats, do not rely on a class allowlist as the only boundary: authenticate the exact serialized bytes before deserialization and protect the keys. A MAC does not encrypt data or prevent replay.
- Do not deserialize untrusted native object streams with Python
pickleor PHPunserialize; prefer a primitive data format such as JSON plus schema and size validation. - For Java serialization that cannot yet be removed, configure an
ObjectInputFilterallowlist and graph limits; filtering is not enabled automatically in every deployment. - For integrity-protected legacy payloads, authenticate the exact bytes before deserialization with a well-managed MAC key; a MAC does not encrypt data or prevent replay.
- Keep session authority server-side or use a maintained, framework-supported signed-token mechanism with expiry, audience, and key-rotation policy.