WAF规则调优:从误伤率到拦截率

一、先说结论:WAF 不是装上去就完事

这两年帮人看站,遇到最多的一句话是「我装了 WAF,怎么还是被打」。很多时候答案不在 WAF 本身,而在规则集和业务之间的错位:默认规则集是给通用站点准备的,它不认识你的后台路径、不认识你那个要上传 ZIP 的接口、也不认识你为了兼容老客户特意保留的 GET 传参接口。结果就是两头都不讨好,要么该拦的没拦,要么把正常用户拦成 403,客服一天收十几封投诉。

这篇文章不讨论该买哪家 WAF,只讨论一件更实际的事:已经有一个 WAF(不管是云上的还是自己用 ModSecurity 搭的),怎么把规则调到一个「能拦住真实攻击、又不误伤业务」的状态。整个过程可以拆成观察、分级、定向收紧、回归验证四步,核心原则只有一句:先看日志再改规则,改一条验一条,永远留一条能回滚的路。

二、第一步永远是观察,不是加规则

新手最常见的操作是登录控制台,把防护等级从「宽松」调到「严格」,然后觉得事情办完了。这一步的风险其实非常高:严格模式下大量规则会进入阻断态,而你还不知道这些规则会命中哪些正常请求。正确的做法是先让 WAF 跑在「只记录不阻断」的模式下,至少攒够一周的日志。

如果你用的是云 WAF,一般都有「观察模式」或「告警模式」开关,把全站规则先切成这个状态。如果是自建的 ModSecurity,把 SecRuleEngine 设成 DetectionOnly:

SecRuleEngine DetectionOnly
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec/audit.log
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial

这里几个参数值得解释一下。DetectionOnly 表示规则照常匹配,但只写日志不返回 403,这是观察期的关键。RelevantOnly 表示只记录触发了规则的请求,不然一个正常站点的访问日志量能把磁盘塞满。AuditLogParts 里带上 B(请求头)和 E(响应体)比较重要,很多误判要看了响应才知道为什么会被拦。

观察期要看的不是「有多少攻击」,而是「哪些规则在正常流量上触发了」。具体做法是按规则 ID 聚合,看命中次数前二十的规则分别是什么、命中的 URL 是什么样:

grep -o 'id "[0-9]*"' /var/log/modsec/audit.log | sort | uniq -c | sort -rn | head -20

一条规则如果一天命中几千次,而对应 URL 全是你的正常业务页面,那它基本可以判定为误报,需要在后面的步骤里做例外。反过来,一条规则一天只命中三五次、URL 全是扫漏洞的路径,那就是真正在干活的规则,不要动它。

三、把命中分成四类,别一锅端

日志拿到手之后,我的习惯是把所有命中分到四个桶里。这个分类比看具体规则号重要得多,因为它决定了后面怎么处理。

第一类是明确的攻击。典型特征是请求路径是 /wp-login.php、/phpmyadmin、/.env、/config.php.bak 这类根本不属于你站点的路径,或者参数里带着 union select、../ 这类明显特征。这类不需要犹豫,直接阻断,而且应该长期阻断。

第二类是扫描和探测。路径存在但方法不对,或者参数形态很怪,比如对你的搜索接口传了几百个字符的单引号。这类请求本身不一定能造成破坏,但它是攻击的前奏,处理方式是限速加临时封禁,不需要默认阻断。

第三类是业务误伤。这是最需要耐心的一类。典型例子:后台富文本编辑器提交 HTML 时带 style 属性,被 XSS 规则拦了;订单备注里用户写了「不知道怎么用 > 这个符号」,被命令注入规则拦了;上传接口传 base64 图片,被规则当成编码攻击拦了。这类要针对具体 URL 和参数做白名单,而不是把整条规则关掉。

第四类是无意义噪声。比如爬虫抓你的页面时带了奇怪的 UA,或者监控探针的请求头不规范。这类只要不影响业务就别管,加例外反而让规则集变复杂。

分类做完,你会得到一张很短的清单:真正需要阻断的规则大概十几条,需要做例外的规则大概五到十条,剩下的都是可以继续观察的。这就是调优的起点。

四、规则收紧的三个具体手法

真正的规则调整,我一般只用三种手法,很少直接去关闭整条规则。

手法一是按 URL 做例外,而不是按规则做例外。ModSecurity 里用 SecRuleUpdateTargetById 可以只修改某条规则在某个上下文里的检查目标:

SecRuleUpdateTargetById 942100 "!ARGS:content"
SecRuleUpdateTargetById 942100 "!ARGS:remark"

这样做的含义是:SQL 注入规则 942100 依然在全局生效,但不再检查 content 和 remark 两个参数。与 SecRuleRemoveById 942100 相比,前者的暴露面小得多。云 WAF 一般也提供「白名单」功能,可以按 URL、参数、来源 IP 组合配置,思路是一样的:把例外限制在最小的范围内。

手法二是把阻断改成挑战或限速。很多规则其实不需要硬拦。对于一个疑似爬虫或者疑似 CC 的请求,返回一个 JS 挑战页面,正常浏览器会自动通过,脚本则卡住。这种方式对用户体验的伤害远小于 403。Nginx 侧可以用 limit_req 先做一个粗粒度的限速,把异常流量压下去,再让 WAF 处理精细规则。

手法三是给攻击面本身做减法。这一条经常被忽略,但性价比最高。后台路径从 /admin 改成随机字符串、把 phpMyAdmin 限制成只允许固定 IP、把 .git 和 .env 的访问在 Nginx 层直接 deny、把不需要 POST 的接口限制成只接受 GET。这些改动不依赖任何 WAF 规则,属于「攻击者根本走不到规则那一步」。我的经验是:先把攻击面收干净,WAF 规则的复杂度能降一半。

五、自定义规则:只写你能说清楚的那几条

调优做到一定程度,会发现默认规则集覆盖不了某些业务特有的异常。这时候可以加自定义规则,但一定要克制。我的标准是:只有当我能用一句话说清楚「什么样的请求一定不是正常用户」时才写规则。

举两个我实际加过的例子。第一个是登录接口的暴力破解:同一个 IP 在五分钟内对 /login 的 POST 超过二十次,直接封禁十分钟。

SecAction "id:1001,phase:1,pass,nolog,initcol:ip=%{REMOTE_ADDR}"
SecRule REQUEST_URI "@beginsWith /login" "id:1002,phase:2,pass,nolog,setvar:ip.login_count=+1,expirevar:ip.login_count=300"
SecRule ip:login_count "@gt 20" "id:1003,phase:2,deny,status:429,log,msg:'login brute force'"

第二个是搜索接口的长度限制。正常用户的搜索词很少有超过一百个字符的,而扫描器往往一次塞几千个字符。在 Nginx 层加一条判断就够了,不必上升到 WAF:

if ($arg_q ~ "^.{150,}$") {
    return 414;
}

写自定义规则时有两个细节容易翻车。一是规则 ID 必须自己规划一个不冲突的段,比如从 1000 开始往上排,别用默认规则集的号段,否则升级规则集时会被覆盖或者冲突。二是每条自定义规则上线前,先用日志回放验证一遍,确认它在过去一周的真实流量里命中次数为零或者全部是攻击流量,这一步能省掉大量事后救火。

六、回归验证:改完要能证明自己没改坏

规则改完,一定要做回归。我的做法分两步,先离线后线上。

离线验证用过去的访问日志回放。把一周的 access.log 提取成请求列表,逐条发给测试环境的 WAF,统计被拦的比例。正常情况下,被拦的比例应该在千分之一以下,而且被拦的请求你能逐条解释为什么该拦。如果某条规则导致的拦截量突然上升,说明这次改动有问题。

线上验证更简单也更关键:切换到阻断模式后,盯住三个数字。第一个是 403 的返回率,如果从 0.1% 跳到了 2%,几乎是必然误伤。第二个是业务侧的错误率,尤其是表单提交和上传接口。第三个是搜索和登录这两个功能,它们是各类规则最喜欢误伤的地方。这三个数稳住了,才能说这次调优是成功的。

还有一条经验:不要一次改十条规则。每次改动只动一到两条,改完观察一天。批量改动的结果是出了问题根本定位不到是哪条引起的,最后只能全部回滚,白干一场。

七、维护节奏:规则集要跟着业务一起变

规则调优不是一次性工作。业务上线新功能、换个新的编辑器、接入第三方支付回调,任何一个动作都可能让原本正确的规则变成误伤源。所以我给自己定的节奏是:每月看一次规则命中排行,每季度做一次完整的观察期,每次大版本更新前先跑一遍离线回放。

规则集本身的升级也要谨慎。厂商的规则包更新后,经常会有新的规则默认进入阻断态。正确的顺序是:先升级、保持 DetectionOnly、观察三到五天、看新命中、做例外、再切阻断。这个顺序反了就很容易在半夜收到告警。

最后说一句实在话。WAF 能挡住的是那些已知特征的攻击,也就是自动化扫描器和脚本小子。真正有针对性的攻击,尤其是逻辑漏洞和业务层面的越权,WAF 基本无能为力。所以不要把 WAF 当成安全的全部,它只是众多层里比较便宜、见效比较快的那一层。

八、小结

WAF 调优的本质是一次次把「规则的通用假设」和「业务的具体形态」对齐。先观察再动手,按四类分开处理,用最小的例外面代替整条规则关闭,改一条验一条,最后把攻击面本身收干净。这套流程并不复杂,但每一步都需要耐心看日志,而这恰恰是很多人跳过的部分。

Last modification:November 3rd, 2026 at 12:24 pm

Leave a Comment