关闭错误回显不足以防御报错回显类盲注,因攻击者利用extractvalue等函数主动触发错误并嵌入数据到错误消息中,数据库会原样返回;须从查询构造、权限控制、响应处理三方面同步加固。

报错回显类盲注不能靠“关错误”一招防住,必须从查询构造、权限控制、响应处理三处同时切断攻击链。
为什么关闭错误回显还不够
很多团队以为只要在生产环境把 show_errors 设为 false 或捕获异常不输出堆栈,就能挡住报错注入。但攻击者根本不需要你“显示错误”——他们用 EXTRACTVALUE()、UPDATAXML()、GTID_SUBSET() 这类函数主动触发错误,并把想读的数据塞进错误消息体里。MySQL 5.7+ 默认允许这些函数在普通用户权限下调用,只要 SQL 语法合法,数据库就会把拼进去的子查询结果原样吐进错误提示中。
常见现象:SELECT * FROM users WHERE id = 1 AND (SELECT EXTRACTVALUE(1, CONCAT(0x7e, (SELECT password FROM users LIMIT 1), 0x7e))) —— 页面没报错,但返回了 500 状态 + 错误消息里含 ~password123~。
- 这类攻击不依赖应用层是否打印异常,只依赖数据库是否执行并回传错误内容
- WAF 对
EXTRACTVALUE类函数识别率低,尤其当参数被编码或拆分时 - ORM 框架(如 Sequelize 的
query())若未启用参数绑定,照样中招
必须禁用高危报错函数与低权限账户
单纯过滤关键词(如拦截 EXTRACTVALUE)容易被绕过(比如用 exTRACTvaLUE、大小写混用、URL 编码)。更可靠的做法是:在数据库层面禁止执行这些函数,或让应用连接账户无权调用它们。
- MySQL 中执行:
REVOKE EXECUTE ON FUNCTION mysql.EXTRACTVALUE FROM 'app_user'@'%' - PostgreSQL 中禁用:
REVOKE EXECUTE ON FUNCTION pg_sleep(text) FROM app_role - 创建专用只读角色,仅授予
SELECT权限,且明确REVOKE所有带副作用的函数 - 检查
information_schema访问权限:普通应用账户不应能查SCHEMATA或TABLES
预编译语句必须覆盖所有动态路径
报错注入常出现在看似“安全”的地方:排序字段、分组条件、动态表名。开发者以为只有 WHERE 需要参数化,但其实任何用户可控的 SQL 结构片段都可能成为入口。
- 错误写法:
ORDER BY ?—— 占位符不能用于列名/表名/关键字,会直接报错 - 正确做法:对排序字段做白名单校验,例如只允许
['id', 'created_at', 'name'],再拼入 SQL - 动态表名必须走配置映射,绝不可来自
req.query.table直接插值 - 使用 ORM 时警惕
raw()、queryRawUnsafe()、knex.raw()—— 这些 API 不自动参数化,必须手动传replacements或bindings
响应体与状态码需统一脱敏
攻击者不仅看错误消息,还观察 HTTP 状态码、响应长度、响应时间的细微差异。比如 500 和 200 返回不同长度,就能辅助布尔盲注;而 500 响应中是否含 ~ 字符,又能辅助报错提取。
- 所有数据库异常统一转为
500+ 固定空响应体(不要返回{"error": "internal"}这种可被长度猜解的结构) - 避免在日志中记录原始 SQL 或错误详情,尤其不要把
err.Error()写进 access log - 前端不要根据后端返回状态码做分支逻辑(如 “500 就重试”),这等于帮攻击者建侧信道
真正难防的不是某条 EXTRACTVALUE 语句,而是开发时默认“这个字段不会进 SQL 结构”的侥幸心理。只要有一个动态表名、一个未校验的排序字段、一个裸调的 raw() 查询,报错注入就可能穿透所有 WAF 和 ORM 的防护层。











