PDO::prepare() 单独调用不防注入,必须配合占位符、bindValue()、禁用模拟预处理、正确编码及结构化参数白名单。

用了 PDO::prepare() 就安全?不一定。很多线上漏洞就出在“以为用了预处理就万事大吉”,结果仍被注入,比如传入 id=1 OR 1=1 后查出全表数据。
为什么 PDO::prepare() 单独调用不防注入
常见错误是只写 $pdo->prepare("SELECT * FROM users WHERE name = '{$_POST['name']}'") —— 这根本没用上预处理,字符串拼接发生在 prepare() 调用前,PDO 完全没机会干预。
- 占位符必须出现在 SQL 字符串里:
?或:name,不能是 PHP 变量插值 -
prepare()只编译模板,不执行也不绑定;没调bindValue()、bindParam()或execute([...]),参数还是裸奔 - 如果 DSN 没显式指定
charset=utf8mb4,且连接时未调setCharset(),宽字节注入可能绕过
PDO::ATTR_EMULATE_PREPARES => false 必须显式关闭
MySQL 默认开启模拟预处理(true),PDO 自己把参数拼回 SQL 字符串再发给服务端——这和手动拼接只有毫秒级差别,不是真隔离。
- 真实预处理要求 MySQL 5.7+ / MariaDB 10.2+,且必须设
PDO::ATTR_EMULATE_PREPARES => false - 设为
false后,若遇到"Invalid parameter number"错误,先检查占位符数量和bindValue()序号是否一致,别急着切回true - 验证是否生效:开发环境开 MySQL
general_log,看日志里是出现PREPARE stmt FROM '...'还是直接一条SELECT ... WHERE name = 'xxx'
数字参数不加引号,mysqli_real_escape_string() 完全无效
这是最隐蔽的坑:SELECT * FROM users WHERE id = " . $_GET['id'] 即使套了 mysqli_real_escape_string(),传 id=1 OR 1=1 仍会执行成功——因为数字字段不加引号,转义函数根本不会处理它。
立即学习“PHP免费学习笔记(深入)”;
- 所有用户输入的数字型条件(
id、status、limit偏移)必须走占位符,不能拼进 SQL - 绑定时显式用
PDO::PARAM_INT,例如$stmt->bindValue(1, (int)$_GET['id'], PDO::PARAM_INT) - 别信
(int)强转能防住一切——如果字段是 ENUM 或 tinyint(1) 存布尔值,类型错配可能导致静默截断或逻辑错乱
表名、字段名、ORDER BY 方向无法参数化,白名单是唯一解
PDO 和 mysqli 的参数绑定只对“值”有效,对 SQL 结构部分(如 FROM users、ORDER BY created_at、ASC)完全无能为力。试图用 addslashes() 或正则过滤这类内容,等于在沙上筑塔。
- 字段名白名单必须硬编码且严格比对:
in_array($sort, ['id', 'title', 'created_at'], true),第三个参数true防止字符串/数字隐式转换绕过 -
ORDER BY方向只接受大写'ASC'或'DESC',用strtoupper()+in_array()校验,拒绝'aSc'或'desc '等变体 - 动态表名?宁可拆成多个固定查询,也别拼接。真要支持多表,用配置数组映射:
$table_map = ['user' => 'users', 'post' => 'posts'],再校验键是否存在
真正卡住注入的,从来不是某一行代码,而是从连接配置、占位符书写、类型绑定、到结构化参数白名单的整条链路。漏掉任意一环,攻击者都可能从缝隙里钻进来。



















