Prevention — PHP
PDO prepared statements, the emulated-prepares trap that makes them lie, and Laravel's raw-query escape hatches.
The Rule
Use PDO or mysqli prepared statements with bound parameters. Turn off PDO's prepared-statement emulation. Never concatenate, and treat mysqli_real_escape_string as inadequate — it does nothing in numeric or identifier contexts.
The PHP-specific trap is that PDO emulates prepares by default, which silently reintroduces string-building underneath an API that looks safe.
PDO
<?php
// Concatenation
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
$rows = $pdo->query($sql);
// Escaping is NOT a substitute, and is useless in a numeric context.
$id = mysqli_real_escape_string($conn, $_GET['id']);
$sql2 = "SELECT * FROM products WHERE id = $id"; // no quotes -> no effectThe Emulated-Prepares Trap
By default, PDO's MySQL driver does not send a real prepared statement to the server. It builds the final query string itself, in PHP, by escaping and interpolating your parameters. This normally works — but it means:
- The protection depends on PDO's escaper being correct for the connection charset. Set the charset in the DSN (
charset=utf8mb4) or the historical multi-byte bypass becomes possible again. PDO::PARAM_INTbinding is not honoured server-side, and some edge cases in emulation have produced injectable output.
Set PDO::ATTR_EMULATE_PREPARES => false so the server parses the query once and receives values over the binary protocol, with no PHP-side string-building at all. This is the single most important line in a PHP database setup, and it is off by default.
// Minimum safe DSN + attribute
$pdo = new PDO(
'mysql:host=localhost;dbname=app;charset=utf8mb4',
$user, $pass,
[PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
mysqli
<?php
$result = mysqli_query($conn,
"SELECT * FROM users WHERE name = '" . $_POST['name'] . "'");Laravel / Eloquent
<?php
// DB::raw and whereRaw with interpolation are injectable.
$users = DB::select(
"SELECT * FROM users WHERE name = '" . $request->name . "'");
User::whereRaw("name = '" . $request->name . "'")->get();
// orderByRaw with user input — identifier context
Product::orderByRaw($request->sort)->get();Checklist
- PDO with
ATTR_EMULATE_PREPARES => falseand a charset in the DSN. - Bound parameters for every value; never concatenate.
mysqli_real_escape_stringis not a defence — remove reliance on it.- Laravel: query builder by default;
?bindings for raw fragments; allowlist fororderByRaw. - Identifiers: allowlist mapped to fixed strings.
ERRMODE_EXCEPTIONon, but ensure the handler returns a generic message to the client — see Error-Based.- Never grant the MySQL account
FILE; setsecure_file_priv— see MySQL.
Related
Fingerprinting, syntax, metadata access, file operations and command execution on the most commonly encountered engine.
Working around application-level defences: blocked quotes, stripped keywords, escaped input, and length caps — without a WAF in sight.
Every ORM has an escape hatch to raw SQL, and every one of them is a footgun. Where the abstraction stops protecting you, and which method names to grep for.
Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.