唯一能防住布尔盲注的是强制参数化查询(mysqli_prepare或pdo::prepare),因addslashes和mysqli_real_escape_string在宽字节编码下可被绕过,且对数字型参数完全无效;动态标识符须白名单校验,所有用户输入路径都必须覆盖。

直接上结论:光靠过滤单引号、隐藏错误、统一返回文案,防不住布尔盲注;唯一能切断攻击链的,是让用户输入彻底失去“变成SQL代码”的能力——也就是强制走 mysqli_prepare 或 PDO::prepare。
为什么 addslashes 和 mysqli_real_escape_string 会失效
这两个函数只在特定字符集(如 latin1)下有效。一旦数据库连接使用 gbk 或 big5,攻击者可用 %A1%AA 这类宽字节绕过转义;更关键的是,它们对数字型参数完全无效——比如 id=1 后面直接接 AND 1=1,根本没单引号可逃。
常见错误现象:
- 页面对
id=1 AND 1=1和id=1 AND 1=2返回不同内容(如“存在”/“不存在”) - 日志里查不到报错,但 sqlmap 跑出数据库名和表结构
实操建议:
- 检查当前连接字符集:
mysqli_set_charset($conn, 'utf8mb4'),并确保 MySQL 服务端也设为utf8mb4 - 禁用
addslashes做 SQL 防御,它不是为此设计的 - 数字型参数必须显式类型转换:
$id = (int)$_GET['id'];,但仅限于确定为整数的场景
参数化查询必须覆盖所有分支路径
很多人只在主查询用了 mysqli_prepare,却在权限校验、日志记录、分页计算等次要逻辑里继续字符串拼接,等于留了一扇没锁的后门。
使用场景:
- WHERE 条件、ORDER BY 字段、LIMIT 偏移量(注意:
LIMIT参数不能直接参数化,需先转成 int) - INSERT 的 VALUES、UPDATE 的 SET 子句、DELETE 的条件
示例对比:
// ❌ 错误:拼接 LIMIT $sql = "SELECT * FROM users LIMIT " . $_GET['offset'] . ", 10"; <p>// ✅ 正确:先校验再拼接,或改用预处理 + 绑定 $offset = (int)$_GET['offset']; $sql = "SELECT * FROM users LIMIT ?, 10"; $stmt = mysqli_prepare($conn, $sql); mysqli_stmt_bind_param($stmt, "i", $offset); </p>
容易踩的坑:
-
PDO::prepare默认不开启PDO::ATTR_EMULATE_PREPARES = false,MySQL 会退化为模拟预处理,仍可能被绕过 - 动态表名、列名无法参数化,必须白名单校验:
in_array($table, ['users', 'orders'], true)
消除布尔反馈差异比写代码还关键
即使 SQL 层已加固,如果应用层仍根据 mysqli_num_rows() 返回值输出“存在”/“不存在”,攻击者就能靠这个二元信号做盲猜。
性能与兼容性影响:
- 统一返回
{"code": 200, "data": null}比返回空数组或 404 更安全,但需评估前端是否依赖状态码跳转 - 加随机延时(如
usleep(rand(10000, 50000)))能干扰时间盲注,但会拖慢正常请求,慎用
实操建议:
- 把“是否存在”逻辑下沉到 SQL 层,用
COUNT(*) > 0统一返回布尔值,避免暴露行数 - 敏感接口(如密码重置、邮箱验证)一律走异步队列+邮件/SMS 通知,不即时返回业务结果
- 日志中记录原始参数但脱敏,例如只记
id=***,不记真实值
最常被忽略的一点:PDO 或 mysqli 的预处理对象必须显式 close(),否则连接池里残留的未关闭语句可能被复用,导致参数绑定错乱——这不是理论风险,线上真出过因未 close 导致的盲注绕过案例。











