How evaluating untrusted byte streams and objects in Python (pickle), Java (readObject), and PHP (unserialize) allows attackers to trigger arbitrary execution gadget chains.
Serialization is like disassembling a Lego castle into numbered instructions to mail it in an envelope, and deserialization is rebuilding the castle from the instructions. Insecure deserialization is when the receiver blindly follows any instruction in the envelope—including instructions that tell the builder to set fire to the living room.
An attacker crafts a serialized object containing a magic method (e.g. __reduce__ in Python) configured to call os.system('id').
The attacker base64-encodes the byte stream and submits it via a cookie, session token, or API request payload.
The server passes the raw bytes directly to pickle.loads() or Java's ObjectInputStream.readObject().
Before the application logic even inspects the object, the runtime engine executes the constructor hooks, granting the attacker a shell.
# VULNERABLE: Unpickling Untrusted User Data
import pickle
import base64
from flask import Flask, request
app = Flask(__name__)
@app.route("/api/load_session", methods=["POST"])
def load_session():
cookie = request.headers.get("X-Session-Data")
# FATAL FLAW: pickle.loads executes arbitrary Python bytecode!
# An attacker providing a payload with __reduce__ gets immediate RCE!
raw_bytes = base64.b64decode(cookie)
user_session = pickle.loads(raw_bytes)
return f"Welcome back, {user_session.username}"
# SECURE: Safe Deterministic Data Formats (JSON + Cryptographic Signing)
import json
import hmac
import hashlib
import base64
from flask import Flask, request, abort
app = Flask(__name__)
SECRET_KEY = b"secure-production-secret-key-32b"
def verify_and_load(signed_payload: str) -> dict:
try:
data_b64, signature = signed_payload.split(".", 1)
data_bytes = base64.urlsafe_b64decode(data_b64)
expected_sig = hmac.new(SECRET_KEY, data_bytes, hashlib.sha256).hexdigest()
# Constant-time signature verification prevents tampering
if not hmac.compare_digest(expected_sig, signature):
abort(401, "Invalid session signature")
# Parse purely as primitive data types, NEVER executable code
return json.loads(data_bytes.decode("utf-8"))
except Exception:
abort(400, "Malformed session token")
pickle, Java readObject, PHP unserialize) from untrusted sources.ValidatingObjectInputStream / JEP 290) to reject unauthorized classes.