必须禁用PDO::ATTR_EMULATE_PREPARES,因为默认true时PHP在客户端模拟预处理、拼接SQL,无法实现SQL与参数的物理隔离,宽字节、多语句等场景下防护失效;仅设为false并配合charset=utf8mb4和异常模式,才能启用MySQL服务端原生预处理。

为什么必须禁用 PDO::ATTR_EMULATE_PREPARES
默认情况下,PDO 会开启 PDO::ATTR_EMULATE_PREPARES = true,这意味着它根本没把 SQL 发给 MySQL 做真正的预处理,而是在 PHP 层自己做字符串替换——和拼接 "WHERE name = '$name'" 几乎等价。一旦遇到宽字节、多语句或旧版 MySQL(如 5.6 之前),防护直接失效。
只有关掉它,才能让 MySQL 服务端真正解析 SQL 模板、单独接收参数,实现物理隔离。这是防注入的硬性前提,不是可选项。
创建 PDO 实例时必须显式关闭模拟预处理
不能靠 setAttribute() 后补,必须在构造时传入配置数组,否则连接已建立、模拟模式已生效,再改无效。
-
PDO::ATTR_EMULATE_PREPARES => false这一行必须存在,且值为false - 同时建议加上
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,避免错误静默失败 - DSN 中必须声明字符集,如
charset=utf8mb4,防止宽字节绕过
正确示例:
立即学习“PHP免费学习笔记(深入)”;
$pdo = new PDO(
"mysql:host=localhost;dbname=test;charset=utf8mb4",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
]
);关了模拟预处理后,哪些写法会立刻失效
禁用模拟后,PDO 会把不支持原生预处理的语法直接报错,而不是“悄悄降级”。这些常见写法会直接抛出异常:
- SQL 中含多个语句,如
"SELECT * FROM a; DROP TABLE b"—— MySQL 服务端原生预处理不支持多语句 - 占位符出现在表名、列名、
ORDER BY或GROUP BY位置,如"SELECT * FROM ?"或"ORDER BY ?" - 使用了 MySQL 特有但未被驱动识别的语法(如某些窗口函数在旧驱动中)
这类动态结构必须用白名单校验 + 字符串拼接(但仅限可信范围),不能依赖占位符。
验证是否真的禁用了模拟预处理
光写对配置还不够,得确认生效。最可靠方式是查 PDO 属性:
var_dump($pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)); // 应输出 bool(false)
也可以配合 MySQL 日志观察:开启 general_log 后,真实预处理会显示两条日志(Prepare + Execute),而模拟模式只有一条 Query。
容易被忽略的是驱动版本兼容性——MySQL 5.1.17+ 才完整支持原生预处理,PHP 的 mysqlnd 驱动需 5.3.22+;用旧环境时,即使写了 false,PDO 也可能自动回退到模拟模式且不报错。



















