第十一章 —— WordPress安全实战指南 · 自定义WAF规则

📌 第三部分:自定义WAF规则

🛡️ 为什么要自定义规则?

虽然启用 ModSecurity 并加载了 OWASP Core Rule Set(CRS)就已经拥有了一整套强大的通用防护机制,但对于实际运行中的 WordPress 网站来说,这只是起点,不是终点。

每个站点的业务逻辑、插件组合、访问模式都有所不同,通用规则难以覆盖所有细节,甚至可能产生误拦截(误报)或防护不足(漏报)的情况。

🤔 常见的例子:
  • 某些 WordPress 插件的 AJAX 接口,可能会被 CRS 当作 SQL 注入错误拦截;
  • 正常用户提交的评论中包含某些 HTML 标签,也可能被当作 XSS 攻击拦住;
  • API 接口对接第三方平台时请求体较大,容易被默认规则拦截;
  • 你想屏蔽某些恶意 IP 或特定 User-Agent,而默认规则没有覆盖。
🔧 自定义规则的运用场景:
  • 排除规则误报,避免影响正常用户体验;
  • 补充防护规则,提升对业务接口的针对性防御;
  • 增强安全策略,如速率限制、IP 黑白名单、文件类型控制等;
  • 减少性能浪费,绕过不必要的深度检测(如某些可信URL路径);

🚀 换句话说,自定义规则让你不仅“有护城河”,还能“量身定制城墙厚度”。

在下面的内容中,我将从规则语法、加载顺序、执行阶段、以及多个 WordPress 场景出发,手把手教你如何编写、部署和调试自己的安全规则。

自定义规则存放的位置

自定义规则的存放位置主要有以下几个:

文件位置/方式加载顺序控制适用场景与CRS兼容性
my_custom_rules.conf灵活(靠 Include 的位置)一般用途、测试、自定义站点通用规则中等
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf在 CRS 前白名单、规则排除、调试放行
REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf在 CRS 后自定义增强、补充检测、防漏网

通用自定义规则通常应放在核心规则集(CRS)加载完成之后,以确保你的规则不会被默认规则覆盖。推荐位置:

/etc/nginx/modsecurity/custom_rules/

也可以将规则统一集中在一个文件中,如:

/etc/nginx/modsecurity/custom_rules/my_custom_rules.conf

并通过主配置文件(如 /etc/modsecurity/modsecurity.conf 或 CRS 主文件)使用 Include 引入:

Include /etc/nginx/modsecurity/custom_rules/my_custom_rules.conf

或使用CRS 附带的“钩子点” (Hook Point)实现在核心规则集加载前/后执行自定义规则:

/etc/nginx/modsecurity/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf/etc/nginx/modsecurity/rules/REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf

以上两种方式择其一,不要混用,如果你用了CRS附带的钩子点,那就不要再用Include的方式了,以免规则冲突。

自定义规则的基本结构

ModSecurity 使用自身 DSL(领域特定语言)书写规则,每条规则的基本语法如下:

SecRule VARIABLES OPERATOR [ACTIONS]
  • SecRule:声明这是一个规则。
  • VARIABLES:要检查的数据(如请求参数、Header 等)。
  • OPERATOR:匹配条件(字符串、正则、IP 等)。
  • ACTIONS:规则触发后的操作(如阻断、记录日志、设置变量等)。

常见语法详解

🧩 VARIABLES(变量)

常用变量包括:

  • ARGS:所有GET/POST参数。
  • ARGS:name:指定参数,如 ARGS:username
  • REQUEST_HEADERS:所有请求头。
  • REQUEST_HEADERS:User-Agent:特定请求头。
  • REQUEST_URI:完整的请求路径。
  • REMOTE_ADDR:客户端IP。
  • REQUEST_COOKIES:所有Cookie。
🧩 OPERATOR(操作符)

操作符用于判断是否匹配:

  • @rx:正则匹配。
  • @streq:字符串是否相等。
  • @pm:是否包含在指定的词列表中。
  • @ipMatch:是否匹配IP段。
  • @contains:是否包含某个子字符串。
  • @beginsWith / @endsWith:前缀/后缀匹配。
🧩 ACTIONS(动作)

常用动作包括:

动作含义
id:1000001规则唯一编号(必需)
phase:1执行阶段(1-5,最常用是1、2)
deny拒绝请求
pass放行请求(白名单)
log写入日志
status:403返回HTTP状态码
msg:'xxx'自定义日志消息
severity:2严重等级(0紧急 – 4调试)
t:none不做预处理(可选)
ctl:ruleEngine=Off控制规则行为,如关闭检测

规则加载顺序与执行阶段(Phase)

ModSecurity 规则加载顺序:

  1. 核心配置文件先加载。
  2. REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:适用于白名单排除规则,或一些需要在CRS执行前执行的自定义规则。
  3. CRS 中的各类攻击检测规则(SQL、XSS等)。
  4. REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf:适用于自定义增强规则或补充检测。

执行阶段(phase)对应生命周期:

阶段名称示例变量运用场景
1请求头接收前REQUEST_HEADERS, REMOTE_ADDR阻止恶意 User-Agent、IP 限制
2请求体接收后ARGSREQUEST_BODY检测XSS/SQL注入、上传控制
3响应头处理前RESPONSE_HEADERS防止泄漏敏感Header
4响应体处理后RESPONSE_BODY检测响应中是否包含敏感词
5日志记录阶段一般不使用日志增强、内部状态记录

示例规则

示例1:阻止特定User-Agent访问

SecRule REQUEST_HEADERS:User-Agent "@contains BadBot" \
    "id:1000001,phase:1,deny,status:403,msg:'Blocked BadBot user agent'"

匹配请求头中含有“BadBot”的User-Agent,直接返回403。

示例2:限制某个参数包含特定关键字(防XSS)

SecRule ARGS "<script>" \
"id:1000002,phase:2,deny,status:403,msg:'Potential XSS attack detected'"

示例3:仅对特定IP段启用规则

SecRule REMOTE_ADDR "@ipMatch 192.168.1.0/24" \
    "id:1000003,phase:1,log,msg:'Internal IP matched'"

示例4:排除某参数不触发CRS规则

SecRule REQUEST_URI "@beginsWith /api/v1/upload" \
    "id:1000004,phase:1,ctl:ruleRemoveById=942100,msg:'Skip SQLi check for upload endpoint'"
📎 提示
  • 每条规则必须有唯一 id,建议使用 1000000 以上的编号,避免和CRS冲突。
  • 多条规则可以按业务逻辑分类,写成多个 .conf 文件。
  • 每次修改规则文件后记得 重启Web服务或重新加载WAF配置,确保新规则生效。
  • 可使用 SecRuleEngine DetectionOnly 模式进行规则测试,不会真正阻断。

实战案例:ModSecurity 拦截日志分析与放行操作

1. 日志分析

我们假设你日志中的一条拦截记录如下(来自 /var/log/nginx/error.log):

ModSecurity: Access denied with code 403 (phase 2). 
Match of "rx (?i:(?:\\b(?:select|union|insert|drop|update|delete|create|alter|rename|truncate)\\b))"
against "ARGS:username"
requested filename "/var/www/html/wp-login.php"
[id "942100"] [msg "SQL Injection Attack Detected"] [data "Matched Data: select found within ARGS:username: testselect"] ...

2. 判断是否可以放行?

从日志信息可知:

  • 拦截规则 ID: 942100(SQL注入检测规则)
  • 请求参数: ARGS:username
  • 参数值: testselect
  • 请求路径: /wp-login.php

判断逻辑:

  • 请求路径是 wp-login.php,说明这是一个 WordPress 登录请求;
  • 用户名中包含了 select 字符串,被误识别为 SQL 注入;
  • 如果你确认这不是攻击行为(比如用户名真的是 testselect),可以有条件地放行;

3. 编写规则思路(如何精准放行)

你不应该直接关闭规则 ID 942100,而应该只对特定路径(如 /wp-login.php)和特定参数(如 username)禁用它,否则容易被攻击者绕过。

因此,我们选择在 REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf 文件中添加如下放行规则。


4. 放行规则添加方法(写入 900 文件)

编辑 REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf 文件,添加以下内容:

SecRule REQUEST_URI "@streq /wp-login.php" \
"id:1009001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:username"

解释:

组件含义
REQUEST_URI "@streq /wp-login.php"只匹配 WordPress 登录页
ctl:ruleRemoveTargetById=942100;ARGS:username从规则 942100 中移除 username 参数的检查目标
id:1009001自定义规则 ID,需保证不与其他重复
phase:1在规则加载前移除目标
pass, nolog本规则不拦截,也不记录日志(避免日志污染)

5. 测试方法

  1. 重启 Nginx,确保规则生效;
  2. 使用浏览器访问 wp-login.php,输入 testselect 作为用户名;
  3. 观察是否仍然出现 403
  4. 查看审计日志 /var/log/modsec_audit.log 或错误日志 /var/log/nginx/error.log
$ sudo tail -f /var/log/nginx/error.log | grep 942100

应当发现规则 942100 未被触发。


6. 总结

步骤操作
1️⃣ 日志分析提取规则 ID、URI、参数名、触发内容
2️⃣ 判断放行评估是否为误报(用户行为是否合理、安全性评估)
3️⃣ 写规则思路控制放行范围(只针对特定 URI 和参数,不整体关闭规则)
4️⃣ 规则部署写入 REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
5️⃣ 验证测试使用 curl 或实际操作 + 日志监控

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注