PDO预处理防注入必须同时满足:使用占位符、禁用模拟预处理(ATTR_EMULATE_PREPARES=false)、值参数化(不可拼接表名/字段名),并配合白名单校验和异常模式。

pdo::prepare() 必须配合 bindValue() 或 execute() 才生效
很多人以为只要写了 prepare() 就算防注入了,其实不然。如果只是 $pdo->prepare("SELECT * FROM user WHERE name = '{$_POST['name']}'"),那还是字符串拼接,prepare() 完全没起作用。
真正起效的链条是:写带占位符的 SQL → 调 prepare() → 再用 bindValue()、bindParam() 或 execute() 传值。三者缺一不可。
- 位置占位符(
?)必须按顺序绑定,bindValue(1, $val, PDO::PARAM_STR)中的1是索引,不是数组键 - 命名占位符(如
:name)可用execute(['name' => $val]),但键名必须严格匹配,多一个空格都不行 - 别在
prepare()里拼变量,哪怕你只拼了一个$table,整条语句就退化为不安全状态
PDO::ATTR_EMULATE_PREPARES 必须设为 false
PHP 默认开启模拟预处理(PDO::ATTR_EMULATE_PREPARES => true),这意味着 PDO 自己在 PHP 层做字符串替换,而不是交由 MySQL 服务端真正编译预处理语句。某些字符集组合下(比如连接未设 utf8mb4),攻击者能绕过防护。
正确初始化方式:
立即学习“PHP免费学习笔记(深入)”;
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
- 不设
false,prepare()可能形同虚设,尤其在旧版 MySQL 或宽字节环境(如 GBK)中风险极高 -
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION不是可选,它让错误暴露出来,避免静默失败掩盖问题 - 连接 DSN 中建议显式指定字符集:
mysql:host=localhost;dbname=test;charset=utf8mb4
表名、字段名、ORDER BY 不能参数化,只能白名单校验
prepare() 只保护“值”(WHERE 条件值、INSERT 的列值等),不保护语法结构。所以 "SELECT * FROM {$table}" 或 "ORDER BY {$sort_field} {$order_dir}" 永远不能靠占位符解决。
必须手动白名单控制:
- 字段名校验:
in_array($sort, ['id', 'title', 'created_at'], true),注意第三个参数true启用严格类型检查 - 排序方向只接受
'ASC'或'DESC',且需转大写:strtoupper($dir) === 'ASC' ? 'ASC' : 'DESC' - 表名若需动态,必须从预定义列表中取:
$tables = ['users', 'posts', 'comments']; if (!in_array($table, $tables)) die('Invalid table');
数字参数不加引号,但依然必须用占位符
常见错误是认为 “数字不用加引号,所以可以拼进去”,比如:"WHERE id = " . (int)$_GET['id']。这看似安全,但漏掉了两个关键点:一是类型转换可能失败(如传入 id=1abc 会截成 1),二是逻辑上开了例外口子,容易蔓延到其他地方。
正确做法是统一走占位符 + 显式类型绑定:
- 整数用
PDO::PARAM_INT:$stmt->bindValue(1, (int)$_GET['id'], PDO::PARAM_INT) - 浮点数用
PDO::PARAM_STR更稳妥(因PDO::PARAM_DOUBLE在部分驱动中行为不一致) - 别依赖自动类型推断,
execute([$_GET['id']])传的是字符串,PDO 可能不转类型,导致隐式转换或索引失效
最易被忽略的是:白名单校验和 PDO::ATTR_EMULATE_PREPARES => false 这两处,一旦漏掉,整个预处理防线就存在可利用缺口。



















