Exercise 1 — SQLi
- With the f-string:
OR '1'='1returns ALL events (filter bypass); the DROP TABLE (where multiple statements are allowed) executes and destroys — on Postgres the default driver doesn't allow multi-statement, but the technique (stacked queries) exists in other drivers: same lesson. - With
%s: the payload is a harmless search literal (title LIKE '%x'' OR ''1''=''1%'): 0 results, table intact. Parameterization separates CODE from DATA forever. - The ORM sends the payload as a WHERE parameter: the seat named
'; DROP --simply doesn't exist. (And by the way: DB error messages never reach the client — DEBUG=False.)
Exercise 2 — XSS
- With innerHTML: the alert fires — your API delivered correct data; the vulnerability is in how the client interprets it. An API's XSS surface is its client.
- textContent (or the framework's escape): the payload displays as text. Recorded rule: innerHTML forbidden with user data (and it matches your longstanding dicresoft rule: zero innerHTML!).
- CSP
default-src 'self'blocks inline scripts/event handlers (onerror=): even if the XSS gets in, it doesn't execute — defense in depth (with nonce/hash for your own scripts).
Exercise 3 — SSRF
localhost:6379returns your Redis's response (or error): the attacker uses YOUR server as an internal browser. In the cloud, 169.254.169.254 hands out the instance role's temporary credentials: game over.- Guard:
urlparsefor the scheme (http/https only),getaddrinfoto resolve ALL the host's IPs, reject if any falls in private/loopback/link-local ranges, and fetch the resolved IP (with the Host header) — not a name that can re-resolve differently. - DNS rebinding: the attacker controls a domain that resolves to a public IP during the check and to 127.0.0.1 during the fetch. Resolving and CONNECTING to the same IP (with the Host header) closes the re-resolution window.
Exercise 4 — check --deploy
- Typical ones: HSTS not configured, cookies without Secure, SSL redirect off, SECURE_CONTENT_TYPE_NOSNIFF off, no explicit X_FRAME_OPTIONS.
- With the lesson's block: the warnings disappear (in dev, Secure cookies break local HTTP login: enable them with per-environment env vars — 27).
- The browseable API exposes test forms, serializers with options and HTML in production: surface and convenience for the attacker. Pure JSON in prod (browseable only if DEBUG).
Exercise 5 — Dependencies
- pip-audit almost always finds something in unpinned requirements (CVEs of old Django/Pillow/requests versions). The severe ones: those with a public exploit or affecting exposed endpoints.
- Exact pinning (reproducibility: your 6-month-old build reconstructs identically) + pip-audit in CI (a new CVE in a dependency = an automatic upgrade PR or at least an alert). That is A06 with the sustainable minimum.
Exercise 11 — The three from §11
- The demonstrated exploit:
PATCH /api/profile {"is_staff": true}withfields = "__all__"→ the user self-escalated (the red test documents it). The fix: the profile serializer EXPLICITLY lists display_name/bio/avatar —is_staffnever enters via JSON (only via 21's admin, with 56's audit). The regression test sends the malicious field and asserts the model didn't change. - The measured timing: the
==comparison with 32-char strings: the first 5-8 correct characters are detectable (~50-200 ns of accumulated difference, measurable with thousands of iterations);compare_digestis constant-time — the side channel closed. The lesson from the measurements: the timing exploit is REAL but demands statistics; the fix is free: there is no excuse. - The typo:
requetsinstalls the typosquatter's package (with code that reads your env vars — demonstrable in the lab venv). With hashes in the lockfile: the install fails (the hash doesn't match the public proxy's). 41'spip-auditreports the known CVEs of the real deps.
Exercise 12 — security.txt and runbook
- The security.txt:
Contact: mailto:security@ticketflow.dev
Expires: 2027-09-28T00:00:00.000Z
Encryption: https://ticketflow.dev/security/pgp-key.txt
Preferred-Languages: es, en
Policy: https://ticketflow.dev/security/policy- The leak runbook (excerpt): (1) triage: confirm the leak with the report's test (<24 h); (2) containment: no actively exploitable exposure if the listing requires 21's auth — if it doesn't: a flag that turns off the nested serializer; (3) fix: remove the field from the serializer + a contract test that enumerates the allowed fields (35); (4) disclosure: no confirmed breach (nobody exploited it: 56's audit log shows no anomalous reads) → changelog; with exploitation evidence → 56's 72 h runbook; (5) postmortem: the piece that would have caught it: 35's contract test had it enumerated fields; the action: add "the contract guard verifies the exact field set" to 28's guard.
- The root question's answer is what closes the runbook: with no piece that would have caught it, the gap belongs to the contract guard — and the P1 action adds it. That loop (finding → missing piece → piece installed) is what turns each incident into a fortress.
Professor's summary
- #1 (access control) is per-PR discipline: matrix + regression tests.
- Injection: parameterize always; the ORM already does — the risk is your raw/extra/f-strings.
- XSS lives in the frontend; CSP and textContent/DOMPurify are the layers.
- SSRF: resolve-before-connect and forbidden private ranges.
- check --deploy + pip-audit + browseable off: misconfiguration is the cheapest to close today.
Next: Lesson 23 — Secrets, rate limiting and GDPR.