唯一靠谱做法是使用mysqli_prepare()或pdo::prepare()预处理,因其实现sql结构与数据彻底分离,参数仅作值处理,无法改变语法;其他过滤函数如addslashes()、htmlspecialchars()完全无效。

用 mysqli_prepare() 或 PDO::prepare() 是唯一靠谱做法
不加预处理,光靠 mysqli_real_escape_string() 或正则过滤,等于给门装个纸锁。SQL 注入不是“可能出错”,而是“只要参数可控,必被利用”。mysqli_prepare() 把 SQL 结构和数据彻底分离,数据库引擎在解析阶段就固定了语句骨架,后续传入的参数只能当值,不能改语法。
实操建议:
- 永远用
?占位符,不要拼接变量进 SQL 字符串,哪怕变量是硬编码的数字或枚举值 - 如果必须动态表名或字段名(比如分表、排序字段),只能白名单校验:
in_array($table, ['user_2024', 'user_2025'], true),绝不用mysqli_real_escape_string()处理这类标识符 -
PDO更推荐:默认启用PDO::ATTR_EMULATE_PREPARES = false,否则 PDO 会退化成客户端模拟预处理,失去防护能力
为什么 addslashes() 和 htmlspecialchars() 完全无效
这两个函数根本不在 SQL 解析链路上。addslashes() 只对单引号、反斜杠等加反斜杠,但 MySQL 在某些字符集(如 gbk)下会把 %A1%AA 这类双字节序列误识别为单引号,绕过转义;htmlspecialchars() 只影响 HTML 输出,对数据库查询零作用——它连数据库连接都没进。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
- 用了
addslashes(),测试时看似正常,上线后被' OR 1=1 --或宽字节注入打穿 - 在输出前用
htmlspecialchars(),结果数据库里存了带&的脏数据,反而污染后续逻辑 - 把过滤函数套在
$_GET全局上(如array_map('addslashes', $_GET)),导致所有参数被无差别转义,json 接口或文件名字段直接坏掉
预处理失败时的真实报错怎么查
很多人看到 Call to a member function bind_param() on bool 就懵,其实这是 mysqli_prepare() 返回 false,说明 SQL 本身有语法错误,或表/字段不存在,跟注入无关。真正的注入防护是否生效,不看运行时错误,而看是否坚持用了预处理流程。
排查步骤:
- 先单独执行原始 SQL 字符串(把
?换成真实值),用 phpMyAdmin 或mysql -e验证是否能跑通 - 检查
mysqli连接是否启用了MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT,否则 prepare 失败只返回false不抛异常 - 确认字符集一致:连接层(
SET NAMES utf8mb4)、php 文件编码、数据库表编码三者必须统一,否则prepare可能静默失败
参数类型绑定不匹配会导致查不到数据,不是安全漏洞但很隐蔽
bind_param() 的第一个参数是类型字符串,i(整数)、s(字符串)、d(浮点)、b(BLOB)。如果该用 i 却写了 s,MySQL 会把数字当字符串比对,索引失效,查询变慢甚至返回空——这看起来像业务 bug,实际是参数类型没对齐。
典型场景:
- ID 查询写成
$stmt->bind_param('s', $id),而$id是int类型,MySQL 执行WHERE id = '123',无法走主键索引 - 时间范围查询用
s绑定strtotime()返回的整数,结果变成字符串比较,'1717027200' > '17170272000'判定错误 - 布尔字段在 PHP 是
true/false,但 MySQL 没原生布尔,通常用TINYINT(1),必须用i绑定,不能用s
复杂点在于:这种错不会报错,数据查不出来或不对,调试时容易怀疑 SQL 逻辑或前端传参,很少想到去核对 bind_param() 的类型字母。