Exercises — Security — OWASP Top 10, input validation, authentication vs authorization
Before we start, one picture fixes the mental model that every problem below tests: the trust boundary — the exact line where data you did not create meets code you do trust.

Everything that follows is a variation on: what crosses that red line, and what checks it before it becomes an action?
Level 1 — Recognition
Goal: given a symptom, name the OWASP category.
Recall Solution 1.1
Answer: A01 — Broken Access Control (specifically IDOR, Insecure Direct Object Reference). WHAT happened: the app checked who the user is (they were logged in) but never checked whether this user may act on object 6. WHY it is A01 and not A07: A07 (Auth Failures) is about the login/identity step breaking; here login worked perfectly — it is the permission step that is missing. That is authorization, hence A01.
Recall Solution 1.2
Answer: A03 — Injection, sub-type SQL injection. WHY: untrusted input closes the quote
and adds OR '1'='1', so data became code — the defining property of every injection flaw.
Recall Solution 1.3
Answer: A02 — Cryptographic Failures. WHY: the secret is protected with a fast, unsalted hash — vulnerable to rainbow tables and brute force. The failure is in how the secret is stored, not in identity or permission checks.
Level 2 — Application
Goal: rewrite broken code into a safe version.
Recall Solution 2.1
cur.execute("SELECT * FROM users WHERE email = ?", (email,))WHY this works: the ? is a bound parameter. The driver compiles the query template
first into a fixed parse tree, then attaches email as a pure leaf value. Recall the parent
note's model: with concatenation , the input can add nodes
to the parse tree . With parameterization — the tree
depends on alone, so no character in email can change it.
Recall Solution 2.2
Encode for the HTML-body context:
<script>steal()</script>
WHY: the browser now displays the characters instead of parsing them as a <script>
element. Same bytes of data, neutralized by encoding for the destination context. (If the same
value went into an HTML attribute or a URL, you'd encode differently — context matters.)
Recall Solution 2.3
db.save(user, bcrypt.hash(password, work_factor=12))(bcrypt/argon2 generate and embed a per-user salt automatically.) The two properties:
- Salt — identical passwords hash differently, so precomputed rainbow tables are useless.
- Work factor — the hash is deliberately slow and tunable, making mass guessing economically infeasible.
Level 3 — Analysis
Goal: explain WHY a defense does or does not close the whole class of attack.
Recall Solution 3.1
Answer: identifiers (table/column names) and dynamic ORDER BY / LIMIT clauses. Parameter
binding only works for values (leaves of the parse tree). You cannot bind a column name — so
code like ... ORDER BY " + sortField is still injectable. WHY: binding replaces a leaf;
a table or column name is structure, not a value, so the driver has no slot to bind it. Fix:
allowlist those identifiers against a fixed set (e.g. sortField in {"date","amount"}).
Recall Solution 3.2
WHY the order: authorization is the question "may THIS subject do THIS action?" — it cannot
be answered until the subject is known. Identity (AuthN) supplies the subject; permission (AuthZ)
evaluates a rule about that subject. No subject ⇒ no rule to evaluate. Anonymous access is not
the absence of authorization — it is authorization where the subject is fixed to "anonymous",
and the rule happens to allow it. So the pipeline is always AuthN → AuthZ, even for public pages.
Recall Solution 3.3
A02 — Cryptographic Failures (a secret, the session token, travels in cleartext and can be sniffed) and A07 — Identification & Auth Failures (a stolen session token lets an attacker impersonate the user, defeating identity). WHY two: the transport flaw is cryptographic; the consequence is a broken identity guarantee. One root cause, two distinct impacts — a classic Threat Modeling insight: trace a weakness to all the properties it violates.
Level 4 — Synthesis
Goal: combine multiple defenses to secure one flow end-to-end.
Recall Solution 4.1
Ordered pipeline:
- Transport = HTTPS — defends A02 (token/secret not sniffable). See HTTP and Web Fundamentals.
- Authenticate the session/token — defends A07: establish who (the subject). See Sessions and Tokens (JWT, OAuth).
- Validate
{id}against an allowlist shape (e.g. integer) — defends A03: no injection via the path parameter. - Authorize per-object: load the order, check
order.owner_id == current_user.idand that the user's role permits refunds — defends A01, applying Principle of Least Privilege. - Default-deny if any check is missing/false — return
403.
WHY this order: you cannot authorize (step 4) before you know the subject (step 2), and you
should not trust {id} (step 3) before using it to look up the object. Each layer removes one
attack class; skipping any one re-opens it.
Recall Solution 4.2
allowed = {"name", "price", "created_at"}
if col not in allowed:
raise ValueError("bad sort column")
cur.execute(
"SELECT * FROM items WHERE name LIKE ? ORDER BY " + col,
("%" + term + "%",),
)WHY: term is a value → parameterize it (the %...% wrapping is applied to the bound
value, not to the SQL text, so it can't break out). col is a structural identifier → it
cannot be bound, so allowlist it against a fixed set before interpolating. Two different tools
because they defend two different parse-tree positions.
Level 5 — Mastery
Goal: reason about tradeoffs and adversary economics.
Recall Solution 5.1
Each in doubles the cost per hash, so it halves the guess rate. Going from to is increments: Answer: — the attacker's throughput drops to one-sixteenth. WHY this matters: defenders pay the cost once per login; attackers pay it once per guess. The work factor is a lever that hurts brute force far more than legitimate use — the core economic idea of Cryptography Basics.
Recall Solution 5.2
A 32-bit salt has possible values, and each demands its own precomputed table entry: Answer: the table must grow by a factor of billion — per password. WHY: the salt makes identical passwords hash differently, so a single shared table no longer works; the attacker is forced back to per-user brute force. That is exactly why salting kills rainbow tables.
Recall Solution 5.3
A 6-digit OTP has possibilities. Guessing uniformly at random, the expected number of attempts to hit it is about half the space: With a 5-try lockout, the success probability per session is , i.e. 1 in 200,000. WHY it changes the game: unlimited attempts guarantee eventual success; the lockout turns a certainty into a tiny probability. This is why A07 explicitly flags missing rate-limiting as an authentication failure.
Recall Feynman self-check: what did every level really test?
L1 — naming the door that broke (identity gate A07 vs permission gate A01 vs data-as-code A03). L2 — replacing "clean the note" with "separate the note from the instructions" (parameterize, encode, hash+salt). L3 — noticing the gaps a defense leaves (identifiers, second-order, transport). L4 — chaining AuthN → validate → AuthZ → default-deny into one pipeline. L5 — counting the attacker's cost and realizing math and process must both be present. One thread ties them all: never trust input you didn't generate, and always separate WHO from WHAT.