web学习笔记编程技术学习与分享

PDO 预处理到底防住了什么

2026 年 09 月 14 日约 3 分钟阅读 后端与数据库

「预处理防 SQL 注入」这句话人人都知道,但防住的机制是什么、哪些场景防不住,值得展开讲一遍。

两种预处理模式

真预处理:SQL 模板先发送到 MySQL 编译为语句句柄,参数单独发送,服务端把参数当作纯数据拼进执行计划——数据永远不会被当作 SQL 语法解析,注入链条在这里断掉。

模拟预处理(PDO::ATTR_EMULATE_PREPARES => true,PHP 默认开启):PDO 在客户端做转义再拼接。它依赖驱动对字符集的正确处理,历史上出现过 GBK 编码绕过转义的问题。

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_EMULATE_PREPARES => false,   // 用真预处理
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$st = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$st->execute([$id]);

防不住的场景

预处理只把值参数化,以下位置它无能为力,必须白名单校验:

// 排序字段:不能 bindValue,要白名单
$allow = ['created_at', 'name', 'price'];
$order = in_array($_GET['order'], $allow) ? $_GET['order'] : 'id';

// 表名、列名、LIMIT 都同理
$sql = "SELECT * FROM products ORDER BY {$order}";

另一类是LIKE 模糊查询:参数化了 % 之外的部分,但 %、_ 本身是通配符,用户输入可以构造全表扫描:

$st->execute(['%' . addcslashes($kw, '%_\\') . '%']);

容易忽略的三条配置

  1. ATTR_EMULATE_PREPARES 默认 true,需要显式关闭;
  2. 字符集要在 DSN 里指定(charset=utf8mb4),不要用 SET NAMES,模拟模式下两者可能不一致;
  3. ERRMODE_EXCEPTION 必须开,否则错误被静默吞掉。

性能方面

真预处理每次执行都要往返服务端编译,对同一语句高频执行的批量插入可以用多值合并。对普通业务查询,这点开销可以忽略——安全性远比这点性能重要。

一句话:预处理把「数据和代码的边界」从字符串层面交给了协议层面,这个边界一旦建立,注入就只剩拼模板这一条路。

PHPMySQL安全