4.5.23 · D4Software Engineering

Exercises — Security — OWASP Top 10, input validation, authentication vs authorization

2,590 words12 min readBack to topic

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.

Figure — Security — OWASP Top 10, input validation, authentication vs authorization

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: &lt;script&gt;steal()&lt;/script&gt; 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:

  1. Salt — identical passwords hash differently, so precomputed rainbow tables are useless.
  2. 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:

  1. Transport = HTTPS — defends A02 (token/secret not sniffable). See HTTP and Web Fundamentals.
  2. Authenticate the session/token — defends A07: establish who (the subject). See Sessions and Tokens (JWT, OAuth).
  3. Validate {id} against an allowlist shape (e.g. integer) — defends A03: no injection via the path parameter.
  4. Authorize per-object: load the order, check order.owner_id == current_user.id and that the user's role permits refunds — defends A01, applying Principle of Least Privilege.
  5. 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.

Flashcards

Why can't you bind a column name as a query parameter?
Parameters replace value leaves in the parse tree; a column name is structure, not a value, so it must be allowlisted instead.
Going from bcrypt work factor 10 to 14, what happens to attacker guess rate?
It drops by 2^4 = 16×, to N/16, because each +1 doubles per-hash cost.
By what factor does a 32-bit salt grow a rainbow table per password?
2^32 ≈ 4.29 billion — each salt value needs its own precomputed table.
Expected guesses to hit a random 6-digit OTP with no rate limit?
About (10^6+1)/2 ≈ 500,000.