flawopen.com/SQL Injection/PHP

SQL Injection in PHP

Critical CWE-89 Draft — pending review
ELI5

Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

SQL injection happens when user-controlled input — most often $_GET or $_POST straight from the request — gets inserted directly into a database query's text. PHP's long history of loosely-typed, easy string concatenation and a huge body of outdated tutorials makes this one of the most persistently copy-pasted vulnerable patterns in the language.

Real-world impact

In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.

Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.

Vulnerable vs. fixed

VULNERABLE
// $_GET value straight into the query
$query = "SELECT * FROM users
  WHERE id = " . $_GET['id'];
$result = $mysqli->query($query);
FIXED
// value is bound, never concatenated
$stmt = $pdo->prepare(
  "SELECT * FROM users WHERE id = :id"
);
$stmt->execute(['id' => $_GET['id']]);
$result = $stmt->fetch();

Why the fix works

PDO compiles the query with the :id placeholder first, then binds the value separately when execute() runs — the value is never merged into the query text the database parses, so it can't change the query's structure regardless of what characters it contains.

PHP-specific gotchas

mysqli_real_escape_string() is not a substitute for prepared statements

Escaping functions quote special characters for the current connection's character set, but historically had bypass issues under certain multi-byte encodings, and they only protect the specific spot they're applied to — one missed call anywhere in the codebase reopens the hole. Prepared statements remove the class of bug entirely rather than mitigating it case by case.

PDO vs mysqli placeholder styles differ

PDO supports named (:id) and positional (?) placeholders; mysqli's prepared-statement API only supports positional ?. Mixing the two conventions by copying from the wrong documentation silently fails.

Legacy mysql_* functions still get copy-pasted from old tutorials

The original mysql_query() API (removed in PHP 7) had no parameter binding mechanism at all. Code following an old blog post or Stack Overflow answer using these functions is a strong signal the whole query layer needs rewriting, not patching.

Common misconceptions

"I used addslashes() or escaped quotes, so I'm safe"

addslashes() is not connection-charset-aware and is not a database escaping function — it's unsafe for this purpose even as a mitigation, let alone as a substitute for parameterization.

"It's an old pattern from a popular tutorial, so it must be fine"

A large share of PHP tutorials predate PDO's widespread adoption. Popularity and age of a pattern say nothing about its safety.

"The value comes from a dropdown, so a user can't type anything malicious"

A dropdown constrains the browser's UI, not the actual HTTP request — an attacker can send any value directly, bypassing the form entirely.

How to check if you're affected

grep -rn '\->query(' --include="*.php" . | grep '\$_\(GET\|POST\|REQUEST\)' grep -rn "mysql_query(" --include="*.php" . grep -rn "addslashes(" --include="*.php" .
A static analyzer like Psalm or PHPStan with a security-focused ruleset (e.g. psalm/plugin-taint-analysis) can trace tainted request data into a query sink automatically.

Prevention checklist

FAQ

Is PDO safer than mysqli?

Both are safe when used with prepared statements and bound parameters — the safety comes from parameterization, not from which library you pick.

Do frameworks like Laravel protect me automatically?

Laravel's query builder and Eloquent ORM parameterize by default. Raw query methods (DB::raw()) built with string concatenation bypass that protection the same way as in any other framework.

Is escaping ever an acceptable fallback?

Only for identifiers that can't be bound as parameters (table or column names), and even then it needs a strict allow-list, not general-purpose escaping — never for values, which should always be bound parameters.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization