PHP 8 中防止 SQL 注入的唯一有效方法是使用 PDO::prepare() + execute() 并禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES => false),所有外部输入必须通过占位符绑定,字段名等结构化部分须白名单校验。

PHP 8 中修复 SQL 注入,唯一有效路径是用 PDO::prepare() + execute() 组合,并关闭模拟预处理——其他任何转义、拼接或“差不多安全”的写法,在 PHP 8.0+ 环境下都已被证明可被绕过。
必须禁用 PDO 模拟预处理(PDO::ATTR_EMULATE_PREPARES => false)
PHP 默认开启 PDO::ATTR_EMULATE_PREPARES,此时 prepare 只是 PHP 层字符串替换,参数仍被拼进 SQL,完全失去防护意义。攻击者传入 1 OR 1=1 仍会生效。
- 显式设置为
false,强制交由数据库服务端做原生预编译,确保 SQL 结构与数据物理隔离 - 不设此项时,
$pdo->prepare("SELECT * FROM user WHERE id = ?")看似安全,实则等价于字符串拼接 - MySQL 服务端需支持预处理(默认开启),但 PHP 层不强制走它,就等于没开
所有外部输入必须走占位符,数字字段也不例外
mysqli_real_escape_string() 在 PHP 8.3 已彻底弃用,且对数字上下文完全无效:传入 id=1 OR 1=1 时,因未加引号,转义函数根本不触发,直接执行全表扫描。
- 错误写法:
"SELECT * FROM user WHERE id = " . (int)$_GET['id']—— 类型转换不是防护,只是碰巧过滤了部分字符 - 正确写法:
$stmt = $pdo->prepare("SELECT * FROM user WHERE id = ?"); $stmt->bindValue(1, $_GET['id'], PDO::PARAM_INT); - 字段名、表名、排序方向(
ORDER BY ?)无法绑定,必须用白名单硬校验,例如in_array($_GET['sort'], ['name', 'created_at'], true)
命名参数比问号更可靠,尤其在多条件动态查询中
问号占位符依赖数组顺序,极易错位。比如 WHERE status = ? AND id = ? 却传入 [$id, $status],逻辑彻底颠倒,且无报错。
立即学习“PHP免费学习笔记(深入)”;
- 命名参数天然绑定键名,顺序无关:
:status和:id不会混淆 - 推荐写法:
$stmt = $pdo->prepare("UPDATE users SET email = :email WHERE id = :id"); $stmt->execute([':email' => $email, ':id' => $id]); - 绑定时用
bindParam()(引用传递)还是bindValue()(值传递),取决于变量后续是否变化;多数场景用bindValue()更直观
初始化 PDO 必须配齐三项关键属性
只调 prepare() 不够,没配属性的 PDO 实例就像没上锁的门——结构看似完整,实际形同虚设。
-
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION:让错误立刻抛出,避免静默失败掩盖漏洞 -
PDO::ATTR_EMULATE_PREPARES => false:如前所述,这是防护生效的前提 -
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC:减少后续fetch()返回类型混淆风险,避免意外取到数字索引导致逻辑错乱 - 示例:
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
最易被忽略的点是:预处理只保“值”,不保“结构”。哪怕你绑得再严,只要把用户输入拼进表名、字段名、ORDER BY 或 UNION 子句里,就等于亲手把钥匙交给攻击者。



















