Skip to content
criticalCVSS 9.8CWE-89A03:2021 – Injection

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 / driverStacked?Detail
MSSQL (all common drivers)YesFully supported. The standard route to xp_cmdshell.
PostgreSQL (libpq simple query)YesSupported by default in most frameworks.
PostgreSQL (extended protocol)NoParameterised calls use the extended protocol, which is one-statement-only.
SQLite (exec / executescript)Yesexec() runs multiple statements; prepare() does not.
MySQL (PHP mysqli_query, PDO)NoSingle-statement by default.
MySQL (mysqli_multi_query)YesExplicitly opted into by the application.
MySQL (PDO, emulated prepares)SometimesDepends on PDO::MYSQL_ATTR_MULTI_STATEMENTS.
Oracle (standard drivers)NoNot supported outside anonymous PL/SQL blocks.
Java JDBC (most drivers)Often noallowMultiQueries=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

SQL
-- 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 TABLE on a scratch name shows the same capability as a DROP.
  • 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-shell and --sql-shell will 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.