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 列表
@validateUrlEncodingURL 编码是否合法(识别 %u 等畸形编码绕过)
@validateUtf8EncodingUTF-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:urlDecodeUniURL 解码(含 %u 编码)
t:removeWhitespace / t:compressWhitespace去空白 / 压缩连续空白
t:htmlEntityDecode解码 &amp; &#x3c; 这类 HTML 实体
t:base64DecodeBase64 解码
t:normalisePath / t:normalisePathWin归一化路径(消解 ../ 与多余斜杠)
t:cmdLine把命令行字符串归一化(抵消失空格、引号、路径变形)

六、动作:命中之后做什么

动作分三类。最常用的是破坏性动作(决定请求的最终命运):

破坏性动作效果
deny立即拦截,可配 status:403(或 406 等)
block按当前 SecDefaultAction 设定的方式拦截(更灵活)
pass放行(但记录 / 计数,常用于只告警不拦)
allow放行并跳过后续阶段检测
redirect + location302 跳转到指定地址

非破坏性动作负责记账与上下文:

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。

相关阅读

问题反馈

本站是纯前端静态站,没有后端也没有账号系统,反馈走 GitHub Issues、讨论区或邮件。

粘贴到 issue 或邮件里能帮我更快定位问题,其中不含你输入的任何内容

其它:查看已有反馈 · 邮件反馈(无需 GitHub 账号):278975598@qq.com