Beyond SELECT

Chapter 6
Beyond SELECT
INSERT/UPDATE/DELETE, stacked queries, and second-order SQLi

So far the injections have read data through SELECT queries. But SQLi isn't limited to reads — it can modify data, chain whole new statements, and even lie dormant until triggered later. This chapter covers the write-side and the trickier delivery patterns, including the one that defeats input-time validation entirely. (Defensive framing — own systems / labs only.)

Injection Into Write Statements

Any statement built with user input is injectable, not just SELECT. An UPDATE or INSERT that concatenates input can be subverted to change other columns or rows:

// a "change my display name" UPDATE built by concatenation UPDATE users SET nickname = '$nick' WHERE id = $userid // attacker sets nickname to inject an extra column assignment: nick = x', role = 'admin UPDATE users SET nickname = 'x', role = 'admin' WHERE id = $userid // → the user just promoted themselves to admin (privilege escalation)

The same applies to WHERE clauses on writes: an injectable UPDATE … WHERE id=$id with id = 5 OR 1=1 updates every row. A DELETE with the same flaw deletes the whole table. So injection into a write is an integrity and availability attack, not just confidentiality.

Stacked Queries: Running Entirely New Statements

Some database drivers allow multiple statements separated by a semicolon in one call. Where that's enabled, an attacker can append a completely separate command after the original — turning a read into a write, a drop, or anything else:

// original: a product lookup (a SELECT) SELECT * FROM products WHERE id = $id // stacked injection appends a second, destructive statement: id = 5; DROP TABLE users-- SELECT * FROM products WHERE id = 5; DROP TABLE users--
Stacked queries are often blocked by the driver — but don't count on it
Many common API/driver combinations disable multi-statement execution by default (e.g. the classic MySQL mysql_query/PHP, and many parameterized-query APIs run exactly one statement), which is why the infamous '; DROP TABLE doesn't always work. But others do allow stacking (often SQL Server, PostgreSQL, and MySQL with multi-statements enabled), so it's a real risk, not a myth. The reassuring part isn't "stacking is usually off" — it's that parameterized queries don't just block stacking, they prevent the injection that would attempt it. Never rely on a driver setting as your defence.

Authentication Bypass — the Write/Logic Variant

Chapter 2 showed login bypass via SELECT. Note it ties to the whole Auth course: injection can also create or elevate accounts (the role='admin' trick above), reset another user's password through an injectable update, or satisfy a privilege check — turning a query-construction bug into full account compromise with no credential. SQLi is frequently the first step in a breach, then pivots through these write capabilities.

Second-Order SQLi: The Payload That Waits

The subtlest and most important pattern in this chapter. Second-order (stored) SQLi splits the attack across two requests: the payload is safely stored first, then triggers later when some other code reads it back into a query unsafely.

// Request 1 — register a username containing a payload. // The signup INSERT is parameterized, so storing it is SAFE — no injection here: username = admin'-- // stored verbatim in the users table // Request 2 — later, a DIFFERENT feature reads the stored username and // concatenates it into a NEW query (e.g. "update my profile"): UPDATE users SET ... WHERE username = 'admin'-- ' ← injection fires NOW
Second-order SQLi defeats input-time validation by design — the data comes from your own database
This is why "sanitize/validate on input" is not a sufficient strategy. The malicious value passes through the front door looking harmless (and is often stored via a safe, parameterized insert). The danger only materializes when a second, different code path — which "trusts" data because it came from the database, not the user — concatenates it into a query. Developers routinely treat database data as trusted and parameterize only "user input," leaving these second-order paths wide open. The lesson: treat data as untrusted based on its origin (it ultimately came from a user), not on where it currently sits — and parameterize every query, including those reading your own tables.

The Unifying Point

Reads, writes, stacked statements, and second-order delivery look different, but they're all the same root cause from Chapter 1 — untrusted input parsed as SQL — at different statement types and via different timing. And they all collapse under the same fix: a parameterized query binds input as data regardless of whether the statement is a SELECT or an UPDATE, whether stacking is on or off, and whether the value arrived just now or was read from a table. One discipline, applied to every query, closes all of it (Chapter 7).

Hands-On Exercises

Exercise 1

Show how an injectable UPDATE users SET nickname='$n' WHERE id=$id can be used to (a) escalate the attacker's own privileges and (b) modify every row. Explain why injection into writes is an integrity/availability attack, not just data theft.

📄 View solution
Exercise 2

Explain what stacked queries are and give an example that turns a SELECT into a destructive command. Then explain why '; DROP TABLE doesn't always work, and why "our driver blocks multiple statements" is a weak thing to rely on.

📄 View solution
Exercise 3

Explain second-order SQLi end to end: how a payload is stored safely in request 1 and fires in request 2. Explain precisely why input validation/sanitization fails to stop it, and state the correct principle for deciding what data is "trusted."

📄 View solution

Chapter 6 Quick Reference

  • SQLi isn't only reads — INSERT/UPDATE/DELETE are injectable too: alter other columns/rows, escalate privileges (role='admin'), delete everything
  • Injection into writes is an integrity & availability attack (modify/destroy data), not just confidentiality
  • Stacked queries — a ; appends a whole new statement ('; DROP TABLE users-- ); works only where the driver allows multi-statements
  • '; DROP TABLE often fails because many drivers/APIs run one statement — but some allow stacking; don't rely on the driver setting
  • SQLi ties to the Auth course: create/elevate accounts, reset others' passwords — query bug → account compromise
  • Second-order SQLi — payload stored safely in request 1, fires when a different path reads it into a query in request 2
  • Second-order defeats input-time validation — the data comes from your own DB; trust by origin (a user), not by where it currently sits
  • All variants = one root cause; all close under parameterizing every query (SELECT and writes, reading user input and your own tables)
  • Next chapter: the primary defence — parameterized queries / prepared statements, in full