SQL Injection allows attackers to interfere with database queries, potentially extracting or modifying data.
TL;DR
$queryRawUnsafe, raw(), text() with f-strings all bypass protectionSQL Injection (CWE-89, OWASP A03:2021) occurs when user-supplied input is concatenated directly into a SQL query instead of being bound as a parameter. The database engine parses attacker-controlled tokens as SQL syntax, executing them with the same privileges as the application's database account. The 2024 Verizon DBIR linked 26% of all data breaches to web application attacks that included SQL injection. OWASP A03:2021 (Injection) records 14,000+ CVEs mapped to CWE-89 and notes that 100% of applications tested showed at least one injection finding.
The vulnerability is not limited to legacy PHP codebases. CVE-2025-25257 (Fortinet FortiWeb, CVSS 9.6) was actively exploited in the wild in July 2025, allowing pre-authenticated attackers to chain SQLi with INTO OUTFILE for root RCE. CVE-2024-43468 (Microsoft SCCM, CVSS 9.8) reached CISA's Known Exploited Vulnerabilities catalog in February 2026. CVE-2024-42005 (Django, CVSS 9.8) demonstrated that even high-level ORM calls — not raw SQL — can be injectable when JSON column aliases are constructed from user input.
The attack surface has expanded beyond classic GET/POST parameters. Injection vectors now include GraphQL arguments, HTTP headers logged to databases, JSON body fields, ORM escape hatch functions, and AI/LLM-generated SQL strings. Classic DAST scanners and WAFs frequently miss these newer vectors.
When an application constructs a query by string concatenation, the attacker controls part of the SQL parse tree. The canonical example: a login form that builds SELECT * FROM users WHERE username = ' + input + '. Injecting admin'-- closes the string literal and comments out the password check entirely, bypassing authentication without knowing any credentials.
The injection lifecycle has four stages: (1) identify an injectable parameter by observing response changes to quote characters; (2) fingerprint the database engine via error messages or conditional timing; (3) enumerate schema via information_schema, sysobjects, or DB-native catalog tables; (4) extract target data or escalate to code execution.
The six major variants differ in how data reaches the attacker and what in-band or out-of-band channel is available.
| Variant | Data Channel | Requires | Typical CVSS | Page |
|---|---|---|---|---|
| Union-based | HTTP response (in-band) | Reflected query result, known column count | 9.8 | ↗ |
| Error-based | HTTP response (in-band) | Verbose DB errors reflected | 9.8 | ↗ |
| Blind Boolean | Response diff (inferential) | Page/status/content changes on true vs false | 8.8 | ↗ |
| Time-based Blind | Timing (inferential) | Measurable response delay | 8.8 | ↗ |
| Out-of-band (OOB) | DNS/HTTP callback (OOB) | DB server outbound network access | 9.8 | ↗ |
| Second-order (Stored) | Delayed in-band/OOB | Stored value executed in a second query | 9.1 | → see below |
Union-based is the fastest for data extraction when query results are reflected. Error-based applies when verbose error messages are enabled. Blind Boolean and Time-based cover cases where no direct output is visible. OOB is used when all in-band channels are suppressed or asynchronous. Second-order injection is systematically missed by automated tools.
Understanding database-specific syntax is essential for testing and confirming injection across different backends.
| Feature | MySQL | PostgreSQL | MSSQL | Oracle | SQLite |
|---|---|---|---|---|---|
| Sleep function | SLEEP(N) | pg_sleep(N) | WAITFOR DELAY '0:0:N' | dbms_pipe.receive_message('a',N) | randomblob(N*1000) |
| Version query | @@version | version() | @@version | v$version | sqlite_version() |
| String concat | CONCAT() or space | || | + | || | || |
| Comment styles | # or -- | -- | -- | -- | -- |
| Stacked queries | Partial (PDO) | Yes | Yes | No | Yes |
| Requires FROM | No | No | No | Yes (DUAL) | No |
| Error leak function | EXTRACTVALUE() | CAST(x AS int) | CONVERT(int,x) | CTXSYS.DRITHSX.SN() | LIKE heavy |
| OOB exfil | LOAD_FILE() UNC | COPY TO PROGRAM | xp_dirtree | UTL_HTTP | None native |
Developers often assume that using an ORM eliminates SQL injection risk. That assumption is correct for standard ORM query methods. It breaks down immediately at escape hatch functions that bypass the parameterization layer.
// Prisma — $queryRawUnsafe IS vulnerable
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = ${untrustedInput}` // VULNERABLE
)
// Prisma — $queryRaw with tagged template IS safe
const users = await prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${untrustedInput}`# SQLAlchemy — text() with f-string IS vulnerable
db.execute(text(f"SELECT * FROM users WHERE name = '{user_input}'")) # VULNERABLE
# SQLAlchemy — text() with bound parameters IS safe
db.execute(text("SELECT * FROM users WHERE name = :name"), {"name": user_input})# Django — raw() with f-string IS vulnerable
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'") # VULNERABLE
# Django — raw() with parameters IS safe
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])CVE-2025-64459 (Django, CVSS 9.1) demonstrated that even standard ORM calls can be injectable when user-controlled dictionaries are expanded into filter(**user_input) without validating internal Q() object parameters.
$queryRawUnsafe (Prisma), text() with f-strings (SQLAlchemy), raw() with format strings (Django), and session.createQuery() with string concatenation (Hibernate) all bypass ORM parameterization entirely. Audit every raw query call in your codebase — not just the obvious ones.
In 2022, Claroty Team82 published research showing that five major WAF vendors — Palo Alto, AWS WAF, Cloudflare, F5, and Imperva — failed to inspect JSON syntax for SQL patterns. Modern SQL databases support JSON operators natively, allowing payloads like the PostgreSQL containment operator (<@) or MySQL JSON_EXTRACT() to pass through WAF inspection untouched.
-- PostgreSQL: containment operator — WAF sees JSON, DB executes SQL context
'{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb
-- MySQL: JSON function as injection wrapper
JSON_EXTRACT('{"id": 14}', '$.name') = 'admin' OR 1=1--All five vendors patched after coordinated disclosure in 2022–2023. Older deployments and custom WAF rule sets may remain vulnerable. This bypass is also effective against content-type switching: many WAFs apply SQL injection rules only to application/x-www-form-urlencoded and skip application/json bodies entirely.
Heartland Payment Systems (2008) — SQL injection against the payment processor's internal network compromised approximately 130 million credit and debit card records. At the time, it was the largest data breach ever recorded. Attackers used the initial SQLi to gain a foothold, then installed packet-sniffers on the transaction processing network.
Equifax (2017) — The breach affecting 143 million US consumers included SQLi as part of the attack chain exploiting Apache Struts. Equifax had been warned about the vulnerability months before exploitation. The breach resulted in $700 million in settlements.
MOVEit (2023) — Progress Software's MOVEit Transfer file-sharing platform contained a zero-day SQL injection vulnerability (CVE-2023-34362, CVSS 9.8). The Cl0p ransomware group exploited it across 80% of US corporate victims before a patch was available. Amazon employee data was among the records stolen and sold.
CVE-2024-43468 (Microsoft SCCM, CVSS 9.8) — Unauthenticated SQL injection in the MP_Location service of Microsoft Configuration Manager. Exploitation enables xp_cmdshell, granting full OS command execution on the SCCM server. Synacktiv published a public PoC in November 2024; CISA added this to KEV in February 2026.
Second-order (stored) SQL injection bypasses input validation at the entry point because the payload appears safe when stored. It executes when a separate function retrieves the stored value and re-uses it in a new query without re-parameterizing. Standard DAST scanners cannot detect this without explicit second-URL configuration.
Second-order SQLi stores the payload safely at one endpoint, then retrieves and executes it at a different endpoint. The attack exploits the false assumption that once data is stored, it is "trusted" for future use.
1. Store: POST /api/report/ {"dateRange": "2024-08-15'"} → 200 OK (escaped, stored)
2. Verify: GET /api/report/ExportToExcel → 500 Error (SQL executes)
3. Confirm: POST with {"dateRange": "2024-10-06';--"} → 200 OK on verify
4. Timing: WAITFOR DELAY '0:0:20' via store → 20s delay on verify
5. OOB: xp_dirtree via store → DNS callback with DB_NAME()Common store → verify endpoint pairs include /register → /admin/users, /profile/update → /dashboard, and /api/report/ → /api/report/ExportToExcel. SQLMap handles second-order testing with --second-url.
') appended to the parameter value and observe whether the response changes in status code, content length, or body content.AND 1=1-- (true condition) and AND 1=2-- (false condition). A different response for each confirms boolean-differential injection.' OR SLEEP(3)-- and measure response time. A 3-second delay confirms time-based injection.X-Forwarded-For, User-Agent, Referer, and Cookie values logged to databases are frequently injectable.{"filter": "value"} to {"filter": "value' OR 1=1--"} and observe differences.# sqlmap baseline scan with authentication
sqlmap -u "https://target.com/search?q=1" --batch --dbs
# JSON body injection
sqlmap -u "https://target.com/api/users" \
--data='{"filter":"*"}' --content-type="application/json" \
--technique=BEUST --batch
# Header injection
sqlmap -u "https://target.com/" \
--headers="X-Forwarded-For: *" --level=3 --batch
# Second-order
sqlmap -u "https://target.com/store" \
--data="field=*" --second-url="https://target.com/trigger" --batchBreachVex detects SQL injection through complementary techniques: baseline measurement, quote-appended probing, boolean true/false differential analysis, and proportional timing confirmation for time-based variants.
Parameterized queries bind user input as data, never as SQL syntax. This defense works at the protocol level — it cannot be bypassed by any encoding or injection technique.
# Python psycopg2 — safe
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
# Python SQLAlchemy — safe
db.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})// Node.js pg library — safe
const result = await client.query(
'SELECT * FROM users WHERE username = $1', [username]
);// Java PreparedStatement — safe
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, username);Table names, column names, and ORDER BY direction cannot be parameterized. Use strict allow-lists.
ALLOWED_COLUMNS = {'id', 'name', 'email', 'created_at'}
ALLOWED_DIRECTIONS = {'ASC', 'DESC'}
def safe_sort(column, direction):
if column not in ALLOWED_COLUMNS or direction not in ALLOWED_DIRECTIONS:
raise ValueError("Invalid sort parameter")
return f"ORDER BY {column} {direction}"The application's database account should hold only the permissions it needs: SELECT on read-only tables, INSERT/UPDATE on specific write tables. Never grant FILE (MySQL), xp_cmdshell execute rights (MSSQL), or COPY TO PROGRAM access (PostgreSQL superuser). An attacker exploiting SQL injection can only do what the database account is permitted to do.
Is SQL injection still a threat in 2026? CWE-89 ranks #3 on MITRE/CISA CWE Top 25 (2024). CISA catalogued 4 exploited SQLi CVEs in 2024 alone. CVE-2025-25257 (FortiWeb) was actively exploited in the wild in July 2025.
Does using an ORM protect against SQL injection? Standard ORM methods generate parameterized queries and are safe. Escape hatch functions ($queryRawUnsafe, raw(), text() with f-strings) bypass all protections. CVE-2025-64459 (Django, CVSS 9.1) demonstrated that even non-raw ORM calls can be injectable through internal parameter manipulation.
Can SQL injection bypass a WAF? Yes. The 2022 Claroty Team82 research bypassed five major WAFs simultaneously using JSON operators. Additional techniques include comment injection, URL encoding, keyword fragmentation, and HTTP parameter pollution. WAFs are defense-in-depth, not primary prevention.
What is the single most effective defense? Parameterized queries (prepared statements) at the application layer. CISA's March 2024 Secure by Design Alert explicitly calls on vendors to eliminate SQL injection as a vulnerability class by adopting parameterized queries universally.
In-band SQLi (Union-based, Error-based) returns data directly in the HTTP response. Blind SQLi (Boolean-based, Time-based, OOB) extracts data indirectly by observing response differences, timing, or out-of-band callbacks.
Standard ORM query methods are safe because they generate parameterized queries. However, escape hatches — Django raw(), SQLAlchemy text() with f-strings, Prisma $queryRawUnsafe — bypass all ORM protections and are directly injectable.
The 2024 Verizon DBIR attributed 26% of all data breaches partly to web application attacks including SQL injection. Aikido's 2024 State of SQL Injections report found SQLi in 6.7% of open-source vulnerabilities scanned.
Yes. On MSSQL, an attacker who achieves SQL injection can enable xp_cmdshell via stacked queries and execute OS commands. On PostgreSQL, COPY TO PROGRAM achieves the same with superuser privileges. On MySQL, INTO OUTFILE can write a PHP webshell if the web root is writable.
Yes. CWE-89 is #3 on MITRE/CISA CWE Top 25 (2024). CISA added 4 SQL injection CVEs to its Known Exploited Vulnerabilities catalog in 2024. CVE-2025-25257 (FortiWeb, CVSS 9.6) was actively exploited in the wild in 2025.
Second-order (stored) SQL injection stores a payload at one endpoint and executes it when another endpoint retrieves the stored value without re-parameterizing. DAST scanners see no immediate response change at the injection point, so they cannot detect it without second-URL configuration.
Claroty Team82 (2022) demonstrated that five major WAF vendors — Palo Alto, AWS WAF, Cloudflare, F5, Imperva — did not inspect JSON syntax for SQL patterns. Injecting via PostgreSQL JSON operators or MySQL JSON functions allowed SQLi payloads to bypass WAF rules entirely.
Append a single quote to the parameter value and observe whether the response changes (error, different content, status code change). Then send both AND 1=1-- and AND 1=2-- and compare responses. A difference between the two confirms boolean-differential injection.