SQL Injection
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan, psalm, and semgrep — all specialist static analysis tools that can flag string-interpolated query patterns. Default linters do not catch this automatically, and it won't be caught by the compiler, so d5 is the right anchor. Not d7 because automated tooling is explicitly available and noted.
Closest to 'simple parameterised fix' (e3). The quick_fix is a clear one-component swap: replace $db->query('...{$var}') with PDO prepare/execute. However, common_mistakes note that table/column names, ORMs with raw methods, and second-order injection mean a thorough fix often touches multiple call sites across the codebase, nudging slightly above e1. e3 fits: it's a repeatable pattern replacement, not a full architectural refactor.
Closest to 'localised tax' (b3). The applies_to covers web, cli, and queue-worker contexts broadly, so it's not confined to one spot. However, the fix is a well-understood pattern (prepared statements) that doesn't reshape the architecture. Once parameterised queries are adopted as a convention, the ongoing burden is low — the primary cost is auditing existing query call sites, not a persistent productivity tax on all future work.
Closest to 'serious trap' (t7). The misconception field states that 'escaping quotes is sufficient protection' — a belief held by many competent developers familiar with older PHP patterns (addslashes, mysql_real_escape_string). This directly contradicts the expectation that sanitising input is equivalent to parameterisation. It also contradicts how similar concepts work in other contexts (e.g. HTML escaping is sufficient for XSS). Second-order injection adds another dimension of surprise. Not t9 because the primary defence (prepared statements) is widely documented and taught.
Also Known As
TL;DR
Explanation
SQL Injection occurs when user-supplied data is concatenated into a SQL statement without parameterisation. An attacker can close the intended query early and append their own SQL — extracting every row in the database, bypassing login checks, or calling destructive commands. It is consistently the most exploited web vulnerability. The fix is always prepared statements with bound parameters; input sanitisation alone is not sufficient.
How It's Exploited
# Returns all rows
GET /users?id=1; DROP TABLE users--
# Drops the table (if stacked queries allowed)
Diagram
flowchart TD
INPUT[User Input<br/>' OR 1=1 --] --> CONCAT[String Concatenation<br/>SELECT * FROM users<br/>WHERE name = '' OR 1=1--']
CONCAT --> DB[(Database<br/>returns ALL rows)]
DB --> LEAK[Data Leak]
subgraph Fix
SAFE_INPUT[User Input] --> PREP[Prepared Statement<br/>? placeholder]
PREP --> BIND[Driver escapes<br/>automatically]
BIND --> SAFE_DB[(Database<br/>safe query)]
end
style LEAK fill:#f85149,color:#fff
style SAFE_DB fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using string escaping (addslashes, mysql_real_escape_string) instead of prepared statements — escaping is bypassable.
- Parameterising values but dynamically concatenating table or column names into queries.
- Assuming an ORM makes you immune — raw query methods like DB::statement() still allow injection.
- Forgetting second-order injection: safely stored input can re-enter a query unsafely later.
Code Examples
$id = $_GET['id'];
$users = $db->query("SELECT * FROM users WHERE id = $id");
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => (int) $_GET['id']]);
$user = $stmt->fetch();