如何用 ModSecurity 自定义规则拦截批量采集?
用 ModSecurity 拦截批量采集的最小方案是 6 条规则,先以 DetectionOnly 跑 3–7 天基线再启用;UA 规则仅作首层过滤,须叠加 IP 频次、请求头完整性和行为路径判断,频次超限返回 429,并注意集合按 wor
拦截批量采集,ModSecurity 的最小可用方案是 6 条规则:1 条白名单、1 条 IP 频次限制、1 条 UA 黑名单、1 条请求头缺失检测、1 条分页深度限制、1 条蜜罐链接。先用 SecRuleEngine DetectionOnly 跑 3–7 天基线,再切到 On;频次超限返回 429 而非 403,让正常用户和搜索引擎知道该退避。
为什么只靠 User-Agent 拦不住采集器?
结论:UA 规则只能挡掉低成本的通用脚本,对能自定义请求头的采集器基本无效,必须叠加“频次 + 请求头完整性 + 行为路径”三层判断。
python-requests、scrapy、Go-http-client、okhttp 这类默认 UA 是最好抓的,但改一个 Header 就能绕过。所以 UA 规则要当作第一层过滤而非最终方案:它能拦掉站点日志里 60%–80% 的无脑爬虫,剩下的必须靠 IP 维度的请求计数和行为特征。注意 UA 正则别写太宽,curl、Java 这些关键词会命中部分监控探针和内部调用,务必配合下面说的白名单。
用 IP 集合做 60 秒频次限制怎么写?
结论:用 initcol:IP=%{REMOTE_ADDR} 建 IP 集合、setvar:IP.rq=+1 自增、expirevar:IP.rq=60 设 60 秒过期,再用 @gt 比较阈值,这是 ModSecurity 做限速的标准写法。
SecAction \
"id:1000001,phase:1,nolog,pass,\
initcol:IP=%{REMOTE_ADDR},\
setvar:IP.rq=+1,\
expirevar:IP.rq=60"
# 超限拒绝
SecRule IP:rq "@gt 120" \
"id:1000002,phase:1,deny,status:429,log,\
msg:'RateLimit %{IP.rq} req/60s from %{REMOTE_ADDR}'"
阈值怎么定:先看正常用户的峰值。列表页正常浏览大约 1–3 请求/秒,60 秒内 120 次是相对宽松的起点;正文页可以收到 60 次。两个坑要避开:一是 Nginx + libmodsecurity v3 的集合默认驻留内存且按 worker 独立,计数不跨进程共享,实际有效阈值约为“配置值 × worker 数”,需要按 worker 数折算或改用 Nginx 的 limit_req 做速率层;二是 SecCollectionTimeout 默认 60 秒,显式写 expirevar 更可控。
哪些请求头特征能识别脚本采集?
结论:真实浏览器会带 Accept、Accept-Language、Accept-Encoding 三个头,缺任意一个的组合,采信度高于 UA 判断。
# 缺 UA 或缺 Accept
SecRule &REQUEST_HEADERS:User-Agent "@eq 0" \
"id:1000010,phase:1,deny,status:403,log,msg:'No UA'"
SecRule &REQUEST_HEADERS:Accept "@eq 0" \
"id:1000011,phase:1,deny,status:403,log,msg:'No Accept'"
# 已知爬虫框架 UA
SecRule REQUEST_HEADERS:User-Agent \
"@rx (?:scrapy|python-requests|aiohttp|Go-http-client|libwww-perl|okhttp|HeadlessChrome)" \
"id:1000012,phase:1,deny,status:403,log,msg:'Scraper UA'"
&REQUEST_HEADERS:X "@eq 0" 里的 & 是“计数”前缀,表示该变量出现次数,用来判断“头不存在”。这条对采集器杀伤力比 UA 大得多,因为很多人只改 UA 不改全套头。误杀风险在于部分 API 客户端和 curl 脚本也不带 Accept-Language,所以只拦“连 Accept 都没有 + 频率异常”的组合更稳。
分页深度和蜜罐链接怎么加?
结论:正常用户几乎不会翻到第 300 页,也不会点到页面上不可见的链接,这两条规则的误杀率接近 0。
# 翻页深度限制
SecRule ARGS_GET:page "@rx ^([3-9][0-9]{2}|[0-9]{4,})$" \
"id:1000020,phase:2,deny,status:429,log,msg:'Deep paging %{ARGS_GET.page}'"
# 蜜罐:页面里放一个 display:none 的 <a href="/trap-9f3a/">
SecRule REQUEST_URI "@contains /trap-9f3a/" \
"id:1000021,phase:1,deny,status:403,log,msg:'Honeypot hit'"
蜜罐是唯一能识别“换 IP + 换 UA + 控频率”的高级采集器的手段,因为它的行为本质是遍历 DOM 里所有链接。命中一次就可以直接封,无需累计。
如何避免误杀正常用户和搜索引擎?
结论:把白名单规则放在配置文件最前面,用 ctl:ruleEngine=Off 关掉该事务的整个引擎,比逐条 SecRuleRemoveById 更好维护。
SecRule REMOTE_ADDR "@ipMatch 10.0.0.0/8,192.168.0.0/16" \
"id:1000000,phase:1,pass,nolog,ctl:ruleEngine=Off"
SecRule REMOTE_ADDR "@ipMatchFromFile /etc/modsecurity/googlebot.txt" \
"id:1000003,phase:1,pass,nolog,ctl:ruleEngine=Off"
顺序很关键:同一 phase 内规则按配置文件出现顺序执行,白名单必须在计数和拒绝规则之前。Googlebot 的 IP 段要每天从官方 JSON 更新到 googlebot.txt,只信反向 DNS 验证过的 IP,别信 UA 里写 Googlebot 的请求。自定义规则 id 统一用 1000000 以上段位,避开 CRS 占用的 1–99999,后续用 SecRuleRemoveById 也清晰。
DetectionOnly 观察期看什么日志?
结论:观察期只做一件事——统计每条规则每天命中多少次、命中哪些 IP 和 URI,命中量突然集中在某几个 IP 就说明规则有效,命中分散在大量正常 IP 就说明阈值太紧。
SecRuleEngine DetectionOnly
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHZ
SecAuditLog /var/log/modsec_audit.log
统计命令:grep -c '"id \"1000002\""' /var/log/modsec_audit.log 看总量,再 grep '"id \"1000002\""' ... | grep -o '\[ip "[^"]*"' | sort | uniq -c | sort -rn | head -20 看来源分布。跑满 7 天、覆盖一个完整流量周期后再切 On,切换后头 24 小时盯 429 和 403 的响应码曲线。
整套规则的成本要清楚:ModSecurity 做的是语义层判断,不是速率层,单条正则对大 body 的开销明显,所以 SecRequestBodyAccess 之外的规则尽量放在 phase:1、用 @pm 代替复杂 @rx,真正的洪水流量交给 Nginx limit_req 或 CDN 挡。三层配合才是完整方案:CDN 挡量、ModSecurity 挡特征、日志和蜜罐负责发现新变种。
转载请注明出处,版权归原作者所有。