为什么带连字符的 WordPress slug 会触发 WAF 上的 OWASP CRS(以及如何排除该参数)
发布于 2026年7月27日

症状:WAF 拦了自家站点
审计 24 小时的 WAF 日志,13 个请求被拦。两个是真实攻击(一个在找 .env 的扫描器)。另外 11 个是站点自身的合法流量:Next.js 的 ISR/重新验证调用,带查询串 ?slug=某个很长的带很多连字符的标题。
# 24 小时内按规则的拦截分布
ruleId 942432 → 7 (SQLi - 特殊字符)
ruleId 942431 → 2 (SQLi - 字符异常)
ruleId 930130 → 2 (LFI)
.env 扫描器 → 2 (真实攻击——保持拦截)原因:连字符被算作 SQLi 异常
OWASP CRS 的 SQL 注入规则对特殊字符和字符串的"异常"打分。一个很长的 WordPress slug——很多连字符、数字、拼接的词——会累积足够的分数越过 942431/942432 的阈值,即便没有任何恶意。slug 参数根本不在 CRS 的排除列表里。
OWASP CRS 是打分,而非定论。一个良性但"怪"的值(带连字符的 slug、base64、JWT)会累积分数并变成误报。修复是排除那个具体参数,而非降低全局灵敏度。
修复:在全部 141 个 capability 中排除 slug
在 OCI 上,排除必须应用到 CRS 的 141 个 capability 中的每一个——没有单一开关。你通过 API/CLI 完成:读取策略,把 argumentName 加到每个 capability 的排除里,再带 etag 写回。由于 WAF 很大,内联的 Python 补丁比手工编辑更稳:
import json, subprocess
pid = "ocid1.waaspolicy.oc1..aaaaEXAMPLE"
pol = json.loads(subprocess.check_output(
["oci","waf","web-app-firewall-policy","get","--waf-policy-id",pid]))
data, etag = pol["data"], pol["etag"]
# 把 'slug' 加到每个 protection capability 的 argument-name 排除里
for cap in data["request-protection"]["rules"][0]["protection-capabilities"]:
ex = cap.setdefault("exclusions", {}).setdefault("args", [])
if "slug" not in ex:
ex.append("slug")
subprocess.run(["oci","waf","web-app-firewall-policy","update","--waf-policy-id",pid,
"--request-protection", json.dumps(data["request-protection"]),
"--if-match", etag])坑:如果策略正在被修改,update 会返回 409("being modified")——你必须等待并用刷新后的 etag 重发。我们用 5 个真实的长 slug 做了循环验证:全部返回 200,而 .env 扫描器仍被拦。
经验教训
1. 带连字符的长 slug 会变成 SQLi 误报。CRS 对特殊字符打分;WordPress 固定链接在不恶意的情况下累积分数。
2. 排除该参数,而非降低灵敏度。把 'slug' 加到排除里,能为请求的其余部分保留防护。
3. 在 OCI 上,排除是按 capability(141 个)。没有单一开关;带 etag 的 API 补丁(外加 409 重试)把它应用到全部。
结论
拦自家站点的 WAF,和放攻击进来的 WAF 一样是问题。OWASP CRS 怀疑怪字符串是对的——它只是不知道那个 ?slug= 是文章标题,而非载荷。在 141 个 capability 中排除该参数,既还回了 ISR,又没放弃拦截同一份日志里出现的 .env 扫描器。