PDO预处理能彻底防御SQL注入,因其将SQL结构与用户数据在数据库端分离执行;必须使用正确占位符、禁用模拟预处理、全部用户输入绑定参数,并对无法参数化的部分(如ORDER BY)做白名单校验。

PDO 预处理能彻底修复 SQL 注入,不是因为它“自动转义”,而是因为数据库服务端真正把 SQL 结构和用户数据拆开了执行——只要用对了,攻击者输什么,都只能当值,不能当代码。
占位符必须写在 SQL 字符串里,不能拼接进去
常见错误是把变量插进 SQL 再交给prepare():
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = " . $_GET['id']);
这根本没触发预处理,prepare() 只编译了一条已拼好的语句,等同于手动拼接。
正确写法只允许两种占位符:
-
:name(命名参数),例如"WHERE email = :email" -
?(问号),例如"WHERE status = ?"
"{$id}"、.$id.、$_GET['table'] 直接拼进 SQL 字符串,都等于放弃防御。
必须禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES => false)
PHP 8.1 默认仍开启模拟预处理。这意味着:
-
prepare()和execute()全在 PHP 层完成 - 数据库根本没收到
PREPARE指令 - PDO 自己把
?替换成带引号的字符串再发给 MySQL —— 和addslashes()一样脆弱
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
验证是否生效:var_dump($pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)); 输出 false 才算到位。
所有用户输入必须走绑定,不能漏掉任何一个
$_GET、$_POST、$_COOKIE 里任何进 SQL 的值,都要通过以下任一方式绑定:
-
execute([value1, value2])(问号方式,按顺序) -
execute([':name' => $value])(命名方式,键名要一致) -
bindValue(':name', $value, PDO::PARAM_STR)(显式指定类型) -
bindValue(1, $value, PDO::PARAM_INT)(数字类型必须用PDO::PARAM_INT)
(int) 强转只是截断,不能替代类型绑定;ORDER BY、表名、字段名这些语法位置无法用占位符,必须白名单校验,比如:
$allowed_sort = ['id', 'created_at', 'name']; $sort = in_array($_GET['sort'] ?? '', $allowed_sort) ? $_GET['sort'] : 'id';
真正容易被忽略的点是:关闭模拟预处理 + 占位符写法 + 绑定动作 + 白名单兜底,四者缺一不可。少一个,就可能在某个边缘 case 下崩掉。



















