flawopen.com/sql-injection/Php
SQL Injection in PHP (PDO, mysqli, Laravel)
A variable interpolated into a double-quoted SQL string lets a request parameter rewrite your query and return every row (CWE-89). The safe call for PDO, mysqli and Laravel, why real_escape_string() still leaks, and how to sort without orderByRaw.[1], [2], [3], [4], [10], [6]
Imagine a stage manager’s cue sheet. If an actor can write on the master script, a line meant to fill in a name can become a new direction for the whole crew. Interpolating $email or concatenating a value into SQL edits that script before the database reads it. PDO::prepare() keeps the SQL plan separate from execute() values; mysqli prepared statements do the same, while real_escape_string() only edits text and cannot protect an unquoted LIMIT value. Laravel where() binds values, but whereRaw() and orderByRaw() accept SQL expressions. Treat sort columns as stage cues: choose them from a fixed allowlist before calling orderBy().[1], [2], [10], [4], [6], [7]
Core Concepts & Subsystem Terms
PDO::prepare() and execute()prepare()sends SQL with?or:namemarkers;execute()supplies the values separately, so they can never change the statement. It only helps when the prepared SQL itself contains no interpolated variables.[2], [8]PDO::ATTR_EMULATE_PREPARES- With pdo_mysql, PDO emulates prepares by default: it escapes values client-side and sends one finished string. That is safe when the connection charset is set in the DSN (
charset=utf8mb4). SettingATTR_EMULATE_PREPARESto false uses real server-side prepares and native types.[7], [8], [9] mysqli_real_escape_string()- Escapes quotes, but only matters inside a quoted string. In a numeric or unquoted position, such as
LIMIT $norWHERE id = $id, there is no quote to escape, so0 OR 1=1passes straight through.[10], [1], [6] mysqli::execute_query()- Added in PHP 8.2: prepares, binds and executes in one call,
$db->execute_query($sql, [$a, $b]). It removes the boilerplate that tempted developers to usequery()with interpolation.[3] Laravel whereRaw / orderByRaw- Laravel's
where('email', $email)binds values.whereRaw,selectRaw,orderByRawandDB::raw()insert their string as SQL; they are safe only with?bindings passed as the second argument. Column names inorderBy()are not bound, so they must come from an allowlist.[4], [6]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Source Code: Flaw vs. Secure Implementation
<?php
// UserQueries.php: every query below interpolates request data into SQL
use App\Models\User;
use Illuminate\Support\Facades\DB;
// PDO: the double-quoted string expands $userId before MySQL sees it
function findEmail(PDO $pdo, string $userId): ?string
{
$row = $pdo->query("SELECT email FROM users WHERE id = $userId")->fetch();
return $row['email'] ?? null;
}
// mysqli: escaping protects the quoted value but not the unquoted LIMIT
function findOrders(mysqli $db, string $status): array
{
$status = $db->real_escape_string($status);
return $db->query("SELECT id FROM orders WHERE status = '$status' LIMIT " . $_GET['limit'])
->fetch_all(MYSQLI_ASSOC);
}
// Laravel: whereRaw inserts the interpolated string as SQL
function findByEmail(string $email): ?object
{
return DB::table('users')->whereRaw("email = '$email'")->first();
}
// orderByRaw with a request value is raw SQL
function listUsers(string $sort)
{
return User::orderByRaw($sort)->get();
}
<?php
// UserQueries.php: values are bound, sort columns come from an allowlist
use App\Models\User;
use Illuminate\Support\Facades\DB;
const SORTABLE = ['id' => 'id', 'email' => 'email', 'created' => 'created_at'];
// PDO: named marker, value supplied separately to execute()
function findEmail(PDO $pdo, int $userId): ?string
{
$stmt = $pdo->prepare('SELECT email FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
$row = $stmt->fetch();
return $row['email'] ?? null;
}
// mysqli (PHP 8.2+): prepare, bind and execute in one call
function findOrders(mysqli $db, string $status, int $limit): array
{
return $db->execute_query('SELECT id FROM orders WHERE status = ? LIMIT ?', [$status, $limit])
->fetch_all(MYSQLI_ASSOC);
}
// Laravel: the query builder binds the value
function findByEmail(string $email): ?object
{
return DB::table('users')->where('email', $email)->first();
}
// allowlist lookup; orderBy only ever sees a known column
function listUsers(string $sort)
{
return User::orderBy(SORTABLE[$sort] ?? 'id')->get();
}
Engineering & System Hardening Checklist
- ✓Run Psalm taint analysis in CI and fail the build on
TaintedSql. If using Semgrep, configure a taint rule for untrusted request data reaching SQL calls and test it against vulnerable and hardened examples.[5], [15] - ✓Reject
query(),exec(),whereRaw,orderByRaw,selectRawandDB::raw()calls whose SQL contains a$variableor.concatenation.[2], [3], [4], [16] - ✓Validate request IDs at the boundary (for example,
filter_var($rawId, FILTER_VALIDATE_INT)plus a range check) before passing anintto the query helper; keep scalar types as an additional contract.[13], [11], [12] - ✓Set
charset=utf8mb4in the PDO DSN andPDO::MYSQL_ATTR_MULTI_STATEMENTS => falsein the connection options.[9], [8], [14] - ✓Map sort keys through a constant array allowlist before
orderBy(); never pass request data toorderBy()ororderByRaw().[4], [6]
Sources
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- PHP Manual — Prepared statements and stored procedures (PDO)
- PHP Manual — mysqli::execute_query
- Laravel — Database: Query Builder (raw expressions)
- Psalm — Security analysis (taint analysis)
- OWASP SQL Injection Prevention Cheat Sheet
- PHP Manual — PDO emulated prepared statements
- PHP Manual — PDO_MYSQL driver behavior
- PHP Manual — PDO_MYSQL DSN and charset
- PHP Manual — mysqli::real_escape_string
- PHP Manual — variables from external sources
- Laravel 12 — HTTP request input
- PHP Manual — filter_var and FILTER_VALIDATE_INT
- PHP bug tracker — PDO_MYSQL multi-statement default
- Semgrep — Taint analysis rules
- PHP Manual — double-quoted strings