waf拦不住sql注入的根本原因是其仅解析http请求而无法感知后端sql执行逻辑。它工作在应用层网关,只能对原始请求字符串做正则匹配或简单解码,却看不到代码中$sql拼接过程、数据库驱动解析顺序、参数是否进入非预编译子句(如order by),更无法控制数据库权限或错误回显——真正防御必须依赖参数化查询等代码层措施。

WAF只看HTTP,不看SQL执行逻辑
WAF工作在应用层网关位置,它收到的是原始HTTP请求,比如GET /user?id=1%27%20UNION%20SELECT%20password%20FROM%20users。它能做的只是对这个字符串做正则匹配、解码还原或行为建模。但真正决定“这会不会执行成恶意SQL”的,是后端代码里那行$sql = "SELECT * FROM user WHERE id = " . $_GET['id']——WAF根本看不到mysqli_query()内部怎么拼、怎么传参、用没用prepare()。
常见错误现象:WAF日志显示“已拦截union select”,但攻击者换用id=1' OR 1=1--或id=1/*!50000UNION*/SELECT 1,2,3就成功了。这不是WAF失职,而是它压根没能力判断OR 1=1在WHERE子句里是否构成逻辑翻转。
- WAF无法识别参数是否被拼进
ORDER BY或GROUP BY——这些子句不支持参数化,必须靠白名单校验 - WAF不感知数据库驱动层的解析顺序:比如PHP的
mysql_real_escape_string()在特定编码下失效,WAF完全不知情 - WAF对JSON POST body默认不扫描,除非显式开启
SecRequestBodyAccess On并配置JSON解析器
绕过本质是WAF与后端的解析差异
WAF和后端服务器(Nginx/Apache)、语言运行时(PHP/Java)、数据库(MySQL/PostgreSQL)对同一段数据的解码层级、空格处理、注释识别、大小写规则全都不一样。只要其中任意一环解析结果不同,绕过就成立。
典型例子:id=1%2520UNION%2520SELECT(双重URL编码)。WAF可能只解一层得id=1%20UNION%20SELECT,认为无害;而PHP的$_GET['id']会自动解两层,最终变成1 UNION SELECT,直接执行。
- MySQL接受
/*!50000UNION*/作为可执行语法,但WAF多数规则把它当普通注释跳过 - GBK宽字节注入中,
%A1%AA在WAF眼里是两个独立字节,在MySQL里却组合成一个有效字符,让前面的转义符失效 - 空格替代方案太多:
%09、%0a、/**/、( )、反引号,黑名单很难全覆盖
WAF拦不住非Web入口的注入
SQL注入不只发生在HTTP GET/POST。只要应用代码存在拼接逻辑,任何能触发该逻辑的路径都危险——而这些路径往往不经过WAF。
常见场景:/api/v1/report?sort_field=user_name被拼进ORDER BY子句;后台任务从Redis读取用户ID再查库;定时脚本从CSV文件加载数据后拼SQL;甚至日志系统把异常堆栈里的SQL片段原样写入数据库。
- 内网API、管理后台、CLI工具、消息队列消费者,基本不走WAF
- WAF无法限制数据库账号权限——就算注入成功,若账号只有
SELECT权限,危害也有限;但WAF管不了这个 - 错误回显(如
MySQL Error: You have an error in your SQL syntax)是盲注的基础,WAF不控制display_errors开关
真正防御点永远在代码里
参数化查询不是“可选项”,是唯一能切断输入与SQL语法耦合的位置。只有pg_query_params()、mysqli_prepare()、ORM的where()绑定,才能确保' OR 1=1 --被当作纯字符串值送入数据库缓冲区,而非语法结构的一部分。
WAF日志里反复放行的请求,其实是代码脆弱性的实时告警。比如sort_field参数总带database(),说明它被拼进了ORDER BY——这时候加WAF规则不如立刻上白名单:in_array($field, ['name', 'email', 'created_at'])。
- 别依赖WAF发现漏洞:它只拦流量,不修代码;漏掉一次,就可能拖库
- WAF规则越严,误杀风险越高——用户昵称
O'Connor、订单号ORD-AND-001都会被拦 - 修复优先级永远是:参数化查询 > 最小权限 > 错误屏蔽 > WAF临时拦截
WAF的解析层级、规则覆盖范围、作用域边界,和代码实际执行环境之间,天然存在不可消除的语义断层。这个断层不是配置能填平的,而是架构决定的。










