📌 第三部分:自定义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 规则加载顺序:
- 核心配置文件先加载。
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
:适用于白名单排除规则,或一些需要在CRS执行前执行的自定义规则。- CRS 中的各类攻击检测规则(SQL、XSS等)。
REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf
:适用于自定义增强规则或补充检测。
执行阶段(phase)对应生命周期:
阶段 | 名称 | 示例变量 | 运用场景 |
---|---|---|---|
1 | 请求头接收前 | REQUEST_HEADERS , REMOTE_ADDR | 阻止恶意 User-Agent、IP 限制 |
2 | 请求体接收后 | ARGS 、REQUEST_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. 测试方法
- 重启 Nginx,确保规则生效;
- 使用浏览器访问 wp-login.php,输入
testselect
作为用户名; - 观察是否仍然出现
403
; - 查看审计日志
/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 或实际操作 + 日志监控 |