Coraza 规则详解:从 SecRule 语法到写出第一条防护规则
一句话结论:Coraza 是 Go 语言实现的开源 WAF、ModSecurity 的官方接任者, 规则语法(SecRule)与 ModSecurity 几乎完全兼容,老项目迁移基本是「换引擎、规则照抄」。 核心是一条 SecRule = 变量 + 运算符 + 动作,配合五个处理阶段与 OWASP CRS 规则集, 既能直接套用社区规则,也能自己写针对性规则。
一、Coraza 是什么
Coraza 是一个用 Go 写的、符合 OWASP 标准的 Web 应用防火墙引擎,也是 ModSecurity v3 停止维护后被社区广泛采用的替代品。它最值得说的三点:
- 语法兼容 ModSecurity:过去写的
SecRule、SecAction、SecRuleEngine等指令,以及 OWASP CRS(Core Rule Set)核心规则集, 基本可以原样跑在 Coraza 上,迁移成本极低; - 纯 Go、无 C 依赖:不依赖 libmodsecurity 那个 C 库,交叉编译、容器化、嵌入到 自己的 Go 程序里都顺手;
- 既能当库也能当网关:可以
import进 Go 服务做内联检测,也能通过 Caddy / Nginx 连接器或独立网关(如 warden)部署在流量前面。
它遵循 Apache-2.0 许可,是 OWASP 基金会下的正式项目。
二、一条规则由什么组成
Coraza 的绝大多数防护逻辑都写在一条 SecRule 里。一条最朴素的规则长这样:
SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(sqlmap|nikto|nmap)" \
"id:1001,phase:1,deny,status:403,msg:'known scanner UA'"
它由三个部分构成,顺序固定:
| 组成部分 | 作用 | 上例对应 |
|---|---|---|
| 变量(Variable) | 指定检测哪份数据:请求头、参数、URI、body、IP…… | REQUEST_HEADERS:User-Agent |
| 运算符(Operator) | 指定怎么判定命中,以 @ 开头 | @rx (?i)(sqlmap|nikto) |
| 动作(Action) | 指定命中后做什么,逗号分隔的键值对 | id:1001,phase:1,deny,... |
反斜杠 \ 是续行符,把一条长规则拆成多行更可读。下面分别拆看这三块。
三、变量:你要检测的是哪份数据
变量决定了规则盯着请求的哪一部分。常用的一批:
| 变量 | 含义 |
|---|---|
REQUEST_URI | 完整请求路径(含查询串),最常用 |
REQUEST_LINE | 完整请求行,如 GET /a?x=1 HTTP/1.1 |
REQUEST_HEADERS | 全部请求头;加冒号取单个,如 REQUEST_HEADERS:User-Agent |
REQUEST_BODY | 请求体(POST 表单 / JSON / XML,需开启相应解析) |
ARGS / ARGS_GET / ARGS_POST | 所有参数 / 仅查询串 / 仅表单 |
QUERY_STRING | 仅查询串部分 |
REMOTE_ADDR | 客户端 IP |
RESPONSE_BODY | 响应体(出方向检测,如泄露银行卡号) |
TX | 事务变量,规则之间用 setvar 传递数据 |
变量还能带计数 / 集合语义,比如 &ARGS 统计参数个数,
ARGS:username 只取名为 username 的参数。多个变量用 | 并联:
REQUEST_HEADERS|REQUEST_BODY 表示「头或体任一命中即触发」。
四、运算符:怎么算命中
运算符决定匹配逻辑,都以 @ 开头。最常用的是正则匹配:
@rx <正则表达式> # 正则匹配,命中返回 true
@pm word1 word2 ... # 短语匹配,多关键词「或」关系,性能优于挨个 @rx
@pmFromFile /path/list # 从文件批量读关键词做短语匹配(如扫描器 UA 清单)
其它常用运算符:
| 运算符 | 判定 |
|---|---|
@eq / @gt / @lt / @ge / @le | 等于 / 大于 / 小于 / 大于等于 / 小于等于(数值) |
@contains / @beginsWith / @endsWith | 包含 / 前缀 / 后缀 |
@within | 目标是否在给定集合内(如 IP 在 CIDR 内) |
@ipMatch | 客户端 IP 是否匹配某个 CIDR / IP 列表 |
@validateUrlEncoding | URL 编码是否合法(识别 %u 等畸形编码绕过) |
@validateUtf8Encoding | UTF-8 编码是否合法 |
@detectSQLi | 内置的 SQL 注入检测(CRS 在用) |
@detectXSS | 内置的 XSS 检测 |
@rx 配合 ! | 取反:@rx !... 表示不匹配才命中 |
小提示:@pm 比一串 @rx (a|b|c) 快得多,它内部是 Aho-Corasick 多模匹配;
要匹配几十个扫描器关键词时优先用 @pmFromFile。
五、转换函数(transforms):匹配前先归一化
攻击者常用大小写混合、URL 双重编码、空白穿插来绕过规则。转换函数会在匹配之前
对变量做变换,把变形归一化。写在动作里、用 t: 前缀,可叠加多个:
"id:1002,phase:2,deny,t:lowercase,t:urlDecode,t:removeWhitespace,t:compressWhitespace,@rx (?i)(union\s+select|drop\s+table)"
| 转换函数 | 做了什么 |
|---|---|
t:none | 先清空之前累积的转换(常放最前重置) |
t:lowercase | 转小写,抵消大小写绕过 |
t:urlDecode / t:urlDecodeUni | URL 解码(含 %u 编码) |
t:removeWhitespace / t:compressWhitespace | 去空白 / 压缩连续空白 |
t:htmlEntityDecode | 解码 & < 这类 HTML 实体 |
t:base64Decode | Base64 解码 |
t:normalisePath / t:normalisePathWin | 归一化路径(消解 ../ 与多余斜杠) |
t:cmdLine | 把命令行字符串归一化(抵消失空格、引号、路径变形) |
六、动作:命中之后做什么
动作分三类。最常用的是破坏性动作(决定请求的最终命运):
| 破坏性动作 | 效果 |
|---|---|
deny | 立即拦截,可配 status:403(或 406 等) |
block | 按当前 SecDefaultAction 设定的方式拦截(更灵活) |
pass | 放行(但记录 / 计数,常用于只告警不拦) |
allow | 放行并跳过后续阶段检测 |
redirect + location | 302 跳转到指定地址 |
非破坏性动作负责记账与上下文:
setvar:tx.sql_hits=+1 # 给事务变量计数(配合 > 阈值再拦)
setvar:tx.block_flag=1 # 打个标记,后面规则读到就拦截
capture # 把 @rx 的捕获组存进 TX.0 / TX.1 ...
log / nolog # 是否写日志
auditlog / noauditlog # 是否进审计日志
还有些元数据动作必须给每条规则带上,方便排障:
id:1003 # 规则 ID(必填且全局唯一,CRS 占 900000+ 段,自定义建议 1000-7999)
phase:2 # 处理阶段
msg:'sql injection' # 命中时记录的可读信息
severity:'CRITICAL' # 严重级别(EMERGENCY/ALERT/CRITICAL/ERROR/WARNING/NOTICE/INFO)
tag:'attack-sqli' # 标签,便于归类统计
七、规则链(chain):多条件「且」关系
单条规则只能表达一个「变量 + 运算符」。要表达「A 且 B」用 chain 把多条规则串起来,
只有整条链全部命中才执行最后一条的破坏性动作:
SecRule ARGS_GET:q "@rx (?i)select" "id:2001,phase:2,chain,t:none,t:lowercase"
SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(sqlmap|havij)" "deny,status:403,msg:'sql tool'"
上例含义:只有当参数 q 里出现 select 并且 UA 是扫描器时才拦截——
避免单独一条 select 就把正常带 SQL 关键字的搜索请求误杀。
八、五个处理阶段(phase)
规则按 phase 分布在请求 / 响应的不同阶段,越靠前越省资源:
| 阶段 | 时机 | 适合放什么 |
|---|---|---|
| phase:1 | 请求头刚收完 | 按 IP / UA / Host 做粗筛(最快) |
| phase:2 | 请求体解析完 | 参数、body 的注入 / XSS 检测(最常用) |
| phase:3 | 响应头生成前 | 出方向头改写 |
| phase:4 | 响应体生成后 | 出方向数据泄露检测(卡号、身份证) |
| phase:5 | 日志记录时 | 仅做统计 / 记日志,不拦截 |
把廉价的粗筛放 phase:1、把贵的正则 / 解码放 phase:2,是减少误杀和开销的关键。
九、OWASP CRS:开箱即用的规则集
自己写规则是兜底,真正扛住日常攻击的是 OWASP CRS(Core Rule Set)—— 一套社区维护、覆盖 SQLi / XSS / 文件包含 / 协议违规 / 扫描器指纹等场景的通用规则,随 Coraza 一起分发。启用方式就是在配置文件里 Include 它:
Include /path/to/coraza.conf # 引擎基础配置(SecRuleEngine 等)
Include /path/to/crs-setup.conf # CRS 总开关与调参
Include /path/to/rules/*.conf # 具体规则文件
CRS 的几个实用调参(在 crs-setup.conf 里):
tx.paranoia_level:偏执等级 1~4,越高规则越严、误报也越多,默认 1; 上线初期建议先 1,观察稳定后再上调;tx.blocking_paranoia_level:实际拦截的偏执等级,可低于paranoia_level做到「高级别只记录、低级别才拦」;tx.anomaly_score_block:把「累计异常分超阈值才拦」作为拦截策略,比单条命中就拦更稳; 单条规则命中通常只加分、不直接 deny,由总分决定是否拦截,显著降低误杀。
十、实战:写一个防 SQL 注入的自定义规则
假设搜索接口 /search?q= 老被注入探测,要在 CRS 之外加一条针对性规则。
思路:先归一化、再正则、命中先计数而非直接拦、超阈值再 deny:
# /etc/coraza/custom/search-sqli.conf
SecRule REQUEST_URI "@rx (?i)/search" "id:900100,phase:1,pass,nolog,setvar:tx.on_search=1"
SecRule ARGS_GET:q \
"@rx (?i)(union\s+select|select\s+.*\s+from|or\s+1=1|'\s+or\s+'|drop\s+table|insert\s+into)" \
"id:900101,phase:2,chain,t:none,t:lowercase,t:urlDecode,t:compressWhitespace"
SecRule TX:on_search "@eq 1" \
"deny,status:403,msg:'SQLi in search q',severity:'CRITICAL',tag:'attack-sqli',\
setvar:tx.sql_score=+5"
SecAction "id:900102,phase:2,pass,setvar:tx.sql_score=0"
SecRule TX:sql_score "@ge 5" "id:900103,phase:2,deny,status:403,msg:'SQLi score exceeded'"
这条规则做了三件事:①只在 /search 路径启用,不影响别的接口;
②对 q 参数做大小写无关、解码后的正则匹配,命中先打 sql_score 标记;
③真正拦截的是「分数累计到 5」那条,给正常关键词搜索留了缓冲,避免一次误匹配就 403。
把自定义文件 Include 进主配置即可,无需改动 CRS 本身。
十一、DetectionOnly 与 On:先观察再拦截
SecRuleEngine 是总开关,只有两个值值得记住:
| 取值 | 行为 | 何时用 |
|---|---|---|
DetectionOnly | 只记录、不拦截 | 新规则 / 新站点上线前先跑一段时间 |
On | 命中即按动作处理 | 确认误报可控之后 |
经验之谈:任何新规则、任何新接入的站点,先用 DetectionOnly 跑至少一到两周,
翻审计日志看有没有把正常业务算成攻击(典型误杀:带 select 的搜索、带
< 的富文本编辑、JSON 里的大段文本)。确认误报率可接受,再把对应规则切到
On 或调高 blocking_paranoia_level。直接 On 上线是 WAF 误杀的头号原因。
十二、怎么把它跑起来
Coraza 不是只能当独立盒子,常见四种落地姿势:
| 方式 | 怎么做 | 适合 |
|---|---|---|
| Go 库内联 | import github.com/corazawaf/coraza/v3,在 handler 里 ProcessRequest | 自己写 Go 服务、想内联检测 |
| Caddy 连接器 | 用 coraza-caddy 插件,Caddyfile 里加几行即可 | 已在用 Caddy 反代 |
| Nginx 连接器 | coraza-nginx 动态模块 | 已在用 Nginx、想最小改动 |
| 独立网关 | 如 warden,一个二进制把 Coraza + CC 防护 + 后台打包 | 不想碰配置、要开箱即用的面板 |
以 Caddy 为例,最小配置只是:
{
order coraza before reverse_proxy
}
example.com {
coraza {
directives `
Include /etc/coraza/coraza.conf
Include /etc/coraza/crs/crs-setup.conf
Include /etc/coraza/rules/*.conf
`
}
reverse_proxy 127.0.0.1:8000
}
十三、小结
Coraza 把 ModSecurity 那套成熟的规则体系搬到了 Go 上,迁移几乎零成本。记住这条主线:
| 概念 | 一句话 |
|---|---|
| 一条规则 | 变量 + 运算符(@ 开头)+ 动作(id/phase/deny…) |
| 归一化 | 用 t: 转换函数在匹配前消解变形绕过 |
| 多条件 | chain 表达「且」,TX 变量做跨规则计数 |
| 日常防护 | 直接上 OWASP CRS,别从零手搓 |
| 上线纪律 | DetectionOnly 观察 → 再 On;异常分阈值拦截比单条 deny 稳 |
想看一个把 Coraza 和 CC 防护、IP 归属拦截打包成单文件网关的真实项目,见 warden:一个单文件部署的 Go 反向代理 WAF。