布尔盲注防御核心是切断真假响应信道,必须统一HTTP状态码(全200)、响应结构(如{"data":null,"code":0,"msg":"success"})并禁用字符串拼接SQL,强制使用预编译参数。

布尔盲注不是“能不能测出来”的问题,而是“你的接口已经暴露了可利用的逻辑分支”——只要响应体、状态码或延迟存在真假差异,攻击者就能逐字猜解数据。防御必须切断这个信道,而不是干扰探测。
为什么统一 HTTP 状态码和响应结构是底线
很多开发者以为返回 404 表示“查不到”,返回 200 表示“查到了”,这是布尔盲注最直接的信号源。攻击者发 id=1 AND 1=1 和 id=1 AND 1=2,靠状态码或空/非空响应就能建立布尔信道。
- 所有查询无论是否命中数据,都必须返回
HTTP 200,且响应体结构一致(例如始终是{"data": null, "code": 0, "msg": "success"}) - 业务错误(如用户不存在)不走 HTTP 状态码,而用
code字段区分,比如{"code": 4001, "msg": "user not found", "data": null} - 禁用类似 Laravel 的
findOrFail()或 Django 的get_object_or_404(),改用显式判空 + 统一返回 - 前端 UI 不依赖
response.status做分支渲染,只读data和code
预编译参数是唯一不可绕过的防线
布尔盲注能成立,根本原因是后端用了字符串拼接构造 SQL。哪怕你把响应做得再“均匀”,只要查询本身是动态拼接的,攻击者就能把布尔逻辑注入到 WHERE 条件里,比如 WHERE id = 1 AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1)='a'。
- 所有用户输入进 SQL 前,必须走数据库原生的预编译支持:
sqlite3_prepare_v2()(SQLite)、pg_query_params()(PostgreSQL)、mysqli_prepare()(MySQLi)或 ORM 的参数化方法(如 SQLAlchemy 的session.execute(text("..."), {"param": value})) - 禁止任何形式的字符串格式化:
f"WHERE id = {user_id}"、"WHERE id = %s" % user_id、.format()全部算违规 - 若真需动态表名/列名(极少见),必须白名单硬编码校验,例如
if table_name not in ["users", "orders"]:抛异常,不拼接
关闭错误回显只是基础,不能替代逻辑隔离
关掉 display_errors=Off(PHP)或 show_errors=false(PostgreSQL)能防报错注入,但对布尔盲注完全无效——它不依赖错误信息,只依赖你代码里 if/else 分支产生的输出差异。
- 数据库连接失败、SQL 语法错误等底层异常,应捕获后记录日志,返回通用错误响应(
code: 5000),绝不透出任何 DB 相关字段名、表名、版本号 - 不要试图用“随机延时”混淆响应时间:
usleep(rand(10000, 50000))对统计攻击无效,反而暴露服务端有意识在对抗,提示攻击者“这里值得深挖” - 真正要做的,是让“条件为真”和“条件为假”在 SQL 层不产生可观测差异:避免在 WHERE 中写
IF(..., SLEEP(2), 0),也别在应用层根据cursor.rowcount == 0决定返回什么结构
最难的部分不是加个预编译,而是把整个数据访问层从“按结果分支”改成“按语义统一处理”。一个 SELECT 查询不该决定 HTTP 状态码,也不该决定 JSON 的顶层字段是否存在——这些都得收口到固定协议里。否则,你越补响应层的洞,攻击者越容易从逻辑层打穿。

















