带 OWASP CRS 规则集的 ModSecurity 十分钟就能打开,三天后又被关掉——在管理区的文章保存不了、文件上传坏掉、某位客户因为地址里有个撇号而下不了单之后。「WAF 妨碍干活」这个结论自然而然就冒了出来,而它是错的:那些拦截几乎全都能靠三四条精确的例外解决,全部诀窍在于把它们找对。

别一上来就开启拦截

第一周只做观察。在 /etc/modsecurity/modsecurity.conf 里:

SecRuleEngine DetectionOnly

在这个模式下 WAF 会记录下它本会拦截的一切,而什么都不拦。一周的真实流量——包括你自己在管理区的操作、图片上传和下单——会给出一份真实的误报清单,而不是假想的。只有把那份清单处理完,切到 On 才有意义。

CRS 是怎么工作的:不是一条规则而是总分

这是理解后面一切的钥匙。CRS 几乎从不用单条规则去拦一个请求。每一条触发的规则都会给这个请求加上异常分,当总分越过阈值时才发生拦截。正因如此日志里出现的不是一行而是好几行,而其中最后一行是标识符为 949110 的规则——正是它在累加总分。

实际后果是:例外必须针对那条加分的规则来做,而不是针对 949110。关掉累加规则就等于关掉了整套规则集,WAF 只剩下配置文件里的一行字。

第二个后果是 paranoia 级别。它默认是一,而这就是正确的选择。级别 2 和 3 加入的规则在普通站点上按设计就会产生误报,只有在把第一级完全调好之后才值得开启。

找出肇事的规则

需要的一切都在审计日志(/var/log/modsec_audit.log)和 Web 服务器的错误日志里。按拦截发生的时间去搜:

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

这就是触发规则的频次清单。然后针对某个具体标识符,看看究竟是什么触发了它:

sudo grep -A5 'id "942100"' /var/log/modsec_audit.log | head -40

需要三样东西:规则标识符、参数名(ARGS:content、ARGS:comment)和请求路径。例外就是用这些拼出来的。

常见的嫌疑对象

这份名单在各个站点之间不断重复:

  • 942100——SQL 注入。会在内容较长的文本字段上触发:文章正文、商品描述、评论。普通散文里的引号、括号以及 select 之类的词,在检测器看来就很可疑;
  • 941100 及 941xxx 系列——XSS。它们随所见即所得编辑器一起到来:字段里有 HTML 标签正是它工作的全部意义;
  • 920420——不被允许的 Content-Type。会弄坏 API 和文件上传:默认允许的类型集合很窄,而在较老的版本里 application/json 根本不在其中;
  • 913100——按 User-Agent 判定的扫描器。会连同扫描器一起抓住合法工具:可用性监控、你自己脚本里的 curl;
  • 200002、200004——请求体解析错误。它们通常并不意味着攻击,而是超出了请求体大小限制,也就是上传了一个大文件。

做例外的三种办法

按粗暴程度递增排列。它们全都写进你自己的文件(例如 /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES.conf),而不是写进 CRS 自己的文件:规则集是会更新的,你的改动会随之消失。

把一个参数从一条规则底下摘出来。最精确的选项,也是应当追求的:

SecRuleUpdateTargetById 942100 "!ARGS:content"

只在一个路径上关掉这条规则。当某个具体页面吵闹时正合适——编辑器、导入、评价表单:

SecRule REQUEST_URI "@beginsWith /admin/post" \
    "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

彻底关掉这条规则。最后的手段,而且几乎总是说明原因根本没找到:

SecRuleRemoveById 942100

第一种与第三种的差别很大。第一种情况下,一个字段——文章正文——不再被一条规则检查 SQL 注入;第二和第三种情况下,整个站点都不再被检查。而付出的力气差别大约是五分钟。

改完之后,请检查配置并轻柔地重载:

sudo apachectl configtest && sudo systemctl reload apache2
sudo nginx -t && sudo systemctl reload nginx

能省下一周的顺序

  1. 在 DetectionOnly 下待一周,期间照常工作,包括站点、管理区和上传。
  2. 从审计日志里取规则频次清单。自上而下处理——头三四条就占了九成的噪声。
  3. 对每一条:弄清是哪个参数、在哪个页面。按参数做例外,不要按规则做。
  4. 到这时才 SecRuleEngine On。
  5. 每月一次去看看拦截记录:站点变了,于是出现了新的误报。

恰恰是最后这一条通常会崩掉:审计日志是好几个 G 的文本,没人会用手去读。汇总面板的意义就在于让触发规则的清单、被拦截的请求和生效中的规则集摆在你眼前,而不是要靠 grep 掏出来。它是什么样子,见下面的演示页面。