EXERCISE 3 — Second-order SQLi end to end, and the trust principle ================================================================== SECOND-ORDER (STORED) SQLi — TWO REQUESTS: REQUEST 1 — STORE THE PAYLOAD (looks harmless, often stored SAFELY): - The attacker submits a value containing SQL syntax through a normal feature — e.g. registers a username: admin'-- - The signup INSERT is PARAMETERIZED, so the value is stored VERBATIM and correctly: the row now literally contains username = admin'-- . No injection happens here — storing it is safe, and input validation (if any) may even pass it (it's a "valid" string). - Nothing bad has happened YET. The payload is sitting dormant in the database. REQUEST 2 — TRIGGER (a DIFFERENT code path reads it UNSAFELY): - Later, some OTHER feature reads that stored username and CONCATENATES it into a NEW query — e.g. a "change password" / "update profile" routine: UPDATE users SET password='...' WHERE username = '' With the stored value admin'-- substituted: UPDATE users SET password='...' WHERE username = 'admin'-- ' - NOW the injection fires: the ' closes the string, -- comments out the rest, and the query's meaning is changed (here it targets the row username='admin' instead of the attacker's own, enabling password takeover of admin). The attack executed on the SECOND request, from data the app pulled out of its OWN database. WHY INPUT VALIDATION / SANITIZATION FAILS TO STOP IT: - Input-time defences inspect data as it ENTERS the system (request 1). But in second-order SQLi: * the value is intentionally INNOCUOUS at entry — admin'-- is a plausible string; escaping it for the INSERT (or storing via a parameterized insert) makes storage succeed cleanly. There's nothing obviously malicious to reject without also breaking legitimate data (real names like O'Brien contain quotes). * the DANGER appears in request 2, in a DIFFERENT code path, against data that is NO LONGER "user input" — it's now "a row from our database," which developers tend to TRUST and not re-validate. - So the validation you did at the front door is irrelevant to the query that actually breaks; and any escaping done at input is often UNDONE or mismatched by the time the value is re-used in a new context. Input validation is the wrong layer and the wrong moment. THE CORRECT PRINCIPLE FOR DECIDING WHAT IS "TRUSTED": - Trust by ORIGIN, not by LOCATION. Data that ULTIMATELY ORIGINATED FROM A USER remains untrusted no matter where it currently lives — even after a round-trip through your own database. "It came from the DB" does NOT mean "it's safe"; the DB is just a place a user's value is resting. - Therefore: parameterize EVERY query — not only the ones that read "user input," but also the ones that read your OWN tables. The defence belongs at the QUERY layer (where data meets code), applied universally, not at the input layer applied selectively. WHY PARAMETERIZATION CLOSES IT (Chapter 7): - If request 2's query is UPDATE users SET password=? WHERE username=? , the stored value admin'-- is BOUND AS A DATA VALUE — it's treated as a literal username string (which matches no one) and has zero syntactic power. The quote and comment are just characters. The second-order trigger never fires, because nothing read from the DB is ever parsed as SQL. ONE-LINE TAKEAWAY: Second-order SQLi stores a harmless-looking payload that fires when a different path concatenates it back into a query; input validation can't stop it because the data is trusted-by-location — so trust by ORIGIN and parameterize every query, including reads of your own data.