Skip to content
CWE-89A03:2021 – Injection

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

PHPuser.phpVulnerable
<?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 effect

The 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_INT binding 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

PHPVulnerable
<?php
$result = mysqli_query($conn,
    "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'");

Laravel / Eloquent

PHPVulnerable
<?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 => false and a charset in the DSN.
  • Bound parameters for every value; never concatenate.
  • mysqli_real_escape_string is not a defence — remove reliance on it.
  • Laravel: query builder by default; ? bindings for raw fragments; allowlist for orderByRaw.
  • Identifiers: allowlist mapped to fixed strings.
  • ERRMODE_EXCEPTION on, but ensure the handler returns a generic message to the client — see Error-Based.
  • Never grant the MySQL account FILE; set secure_file_priv — see MySQL.