Module 4 · Authentication and security

Lesson 22 — OWASP Top 10

SQL injection, XSS, CSRF, SSRF and broken access control — with real examples.

Published
In this lesson
  1. Exercise 1 — SQLi
  2. Exercise 2 — XSS
  3. Exercise 3 — SSRF
  4. Exercise 4 — check --deploy
  5. Exercise 5 — Dependencies
  6. Exercise 11 — The three from §11
  7. Exercise 12 — security.txt and runbook
  8. Professor's summary

Exercise 1 — SQLi

  1. With the f-string: OR '1'='1 returns 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.
  2. 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.
  3. 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

  1. 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.
  2. 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!).
  3. 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

  1. localhost:6379 returns 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.
  2. Guard: urlparse for the scheme (http/https only), getaddrinfo to 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.
  3. 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

  1. Typical ones: HSTS not configured, cookies without Secure, SSL redirect off, SECURE_CONTENT_TYPE_NOSNIFF off, no explicit X_FRAME_OPTIONS.
  2. With the lesson's block: the warnings disappear (in dev, Secure cookies break local HTTP login: enable them with per-environment env vars — 27).
  3. 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

  1. 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.
  2. 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

  1. The demonstrated exploit: PATCH /api/profile {"is_staff": true} with fields = "__all__" → the user self-escalated (the red test documents it). The fix: the profile serializer EXPLICITLY lists display_name/bio/avatar — is_staff never 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.
  2. 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_digest is 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.
  3. The typo: requets installs 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's pip-audit reports the known CVEs of the real deps.

Exercise 12 — security.txt and runbook

  1. The security.txt:
text
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
  1. 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.
  2. 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.