增加waf模块

This commit is contained in:
qing committed 2026-07-25 01:27:08 +08:00
1 parent fbccc5d909
commit 202f2804b2
22 files changed
+1091 -11

No files matched your search

+5 -3
View File
@@ -121,10 +121,12 @@
根目录提供一键安装脚本,按依赖顺序依次构建并安装各模块(库先行,core 最后):
```bash
# 1. 安装系统依赖(Debian/Ubuntu)
bash _setup.deb.sh
# 1. 安装系统依赖
bash _setup.deb.sh # Debian/Ubuntu
# 或
bash _setup.rhel.sh # RHEL/CentOS/Rocky/Fedora
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> static -> dnfs -> db -> core)
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> waf -> static -> dnfs -> db -> core)
bash _inst.sh
```
+1
View File
@@ -30,6 +30,7 @@ inst_module "libhttp"
inst_module "libcrypt"
inst_module "libsqlite3"
inst_module "gateway"
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
inst_module "static"
inst_module "dnfs"
inst_module "db"
+1 -1
View File
@@ -2,4 +2,4 @@
# Debian
sudo apt update
sudo apt install libsqlite3-dev libboost-all-dev libcurl4-gnutls-dev python3 python3-venv python3-pip g++ patchelf
sudo apt install libsqlite3-dev libboost-all-dev libcurl4-gnutls-dev python3 python3-venv python3-pip g++ patchelf golang-go
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# RHEL / CentOS / Rocky / Fedora
sudo dnf install -y boost-devel libcurl-devel python3 python3-pip gcc-c++ patchelf golang sqlite-devel
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# Debian
sudo apt update
sudo apt install libboost-all-dev libcurl4-gnutls-dev python3 python3-venv python3-pip g++ patchelf
+136
View File
@@ -0,0 +1,136 @@
# CAPTCHA 验证码服务
一个轻量级的验证码图片服务,仅依赖 Python 标准库实现 HTTP 服务(无需 Flask),配合 Pillow 生成验证码图片。提供图片生成、Token 校验、健康检查等接口,适合作为后端服务的验证码组件。
## 功能特性
- **图片验证码生成**:5 位字符(字母 + 数字),自动排除易混淆字符(`l I 1 O 0 o`)
- **Token 机制**:每次生成图片返回唯一 Token,校验时一次性消费,防止重放
- **过期自动清理**:Token 默认 300 秒(5 分钟)失效,每次请求时顺带清理过期项
- **线程安全**:使用锁保护内存存储,支持并发请求
- **恒定时间比较**:答案校验使用逐字符异或比较,避免计时攻击
- **干扰元素**:随机字符颜色、位置偏移、干扰线与噪点,提升识别难度
- **零框架依赖**:HTTP 层基于 `http.server`,除 Pillow 外无第三方依赖
## 技术栈
- Python 3
- [Pillow](https://python-pillow.org/)(PIL)—— 图片绘制
- Python 标准库:`http.server`、`threading`、`secrets`、`json`、`urllib`
## 安装
```bash
# 安装图片处理依赖
pip install Pillow
```
> 字体默认使用 `/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf`(Linux 常见路径)。若系统中不存在该字体,会自动回退到 PIL 默认字体。
## 运行
```bash
# 方式一:通过启动脚本
./_run.sh
# 方式二:直接运行
python3 captcha_service.py
```
服务启动后监听 `http://127.0.0.1:8009`。
## 配置项
在 `captcha_service.py` 顶部可调整以下常量:
| 常量 | 默认值 | 说明 |
| ------------- | ------------ | --------------------- |
| `CAPTCHA_LEN` | `5` | 验证码字符长度 |
| `CAPTCHA_TTL` | `300` | Token 有效期(秒) |
| `ADDR` | `127.0.0.1` | 监听地址 |
| `PORT` | `8009` | 监听端口 |
## API 接口
### 1. 获取验证码图片
```
GET /captcha/image
```
返回一张 PNG 验证码图片,响应头 `X-Captcha-Token` 携带本次验证的 Token,需在后续校验时一并提供。
**示例:**
```bash
curl -D - http://127.0.0.1:8009/captcha/image -o captcha.png
```
响应头示例:
```
Content-Type: image/png
X-Captcha-Token: 3f9a1c2b4d5e6f7a8b9c0d1e2f3a4b5c
```
### 2. 校验验证码
```
POST /captcha/verify
Content-Type: application/json
```
请求体:
```json
{
"token": "3f9a1c2b4d5e6f7a8b9c0d1e2f3a4b5c",
"answer": "AB3XY"
}
```
响应(校验不区分大小写,Token 校验后即失效):
```json
{ "valid": true }
```
错误情况:
| HTTP 状态码 | 响应体 | 说明 |
| ----------- | --------------------------------------------------- | ----------------------- |
| `400` | `{ "valid": false, "error": "invalid json" }` | 请求体非合法 JSON |
| `400` | `{ "valid": false, "error": "token and answer required" }` | 缺少 token 或 answer |
| `400` | `{ "valid": false, "error": "invalid or expired token" }` | Token 无效或已过期 |
### 3. 健康检查
```
GET /health
```
```json
{ "status": "ok", "service": "captcha" }
```
## 典型使用流程
1. 客户端调用 `GET /captcha/image`,保存返回的图片与响应头中的 `X-Captcha-Token`。
2. 用户根据图片内容输入验证码文本。
3. 客户端调用 `POST /captcha/verify`,提交 `token` 与用户输入的 `answer`。
4. 服务端比对答案(小写化、恒定时间比较),返回 `valid` 结果,Token 随即作废。
## 项目结构
```
captcha/
├── captcha_service.py # 服务主程序(HTTP 服务 + 验证码生成与校验)
├── _run.sh # 启动脚本
└── README.md
```
## 注意事项
- 验证码存储在进程内存中,服务重启后所有 Token 失效,不适合多实例水平扩展场景(如需分布式可替换为 Redis 等外部存储)。
- 默认仅监听 `127.0.0.1`,仅供本机或内网调用;如需对外暴露请配合反向代理并做好限流。
- 答案校验不区分大小写,Token 一经校验即从内存中移除(无论对错)。
+1
View File
@@ -2,5 +2,6 @@ bash /opt/micro_servers/ai_review/_run.sh
bash /opt/micro_servers/db/_run.sh
bash /opt/micro_servers/dnfs/_run.sh
bash /opt/micro_servers/static/_run.sh
bash /opt/micro_servers/waf/_run.sh
bash /opt/micro_servers/gateway/_run.sh
bash /opt/micro_servers/captcha/_run.sh
+6 -2
View File
@@ -1,6 +1,10 @@
[Server]
address = 0.0.0.0
port = 443
# 当 WAF 在前面终止 TLS 时,gateway 退化为内部 HTTP 服务:
# - tls = off 启用 HTTP 模式(配合 waf 模块使用)
# - tls = on 启用 HTTPS 模式(独立部署,证书为 full_chain.crt / server.key)
address = 127.0.0.1
port = 8443
tls = off
[Proxy]
default = http://127.0.0.1:9200
+8
View File
@@ -280,6 +280,7 @@ bool is_special_header(const std::string &header);
/* configure items */
static std::string _ADDR;
static int _PORT;
static bool _TLS = false;
static std::string _DEFT_URL;
static std::vector<std::string> _OPEN_URL;
static std::string _DB_PATH;
@@ -385,6 +386,7 @@ int main(int argc, char **argv) {
ConfIni cf(vm["input"].as<std::string>());
_ADDR = cf.get<std::string>("Server.address", "");
_PORT = cf.get<int> ("Server.port", -1);
_TLS = cf.get<bool> ("Server.tls", false);
_DEFT_URL = cf.get<std::string>("Proxy.default", "");
_OPEN_URL = split(cf.get<std::string>("Proxy.opened", ""), ';');
_DB_PATH = cf.get<std::string>("Database.path", "");
@@ -402,7 +404,13 @@ int main(int argc, char **argv) {
return 0;
}
// 根据 Server.tls 选择 HTTPS(带证书)或 HTTP(无证书)构造函数
// HTTP 模式用于 WAF 终止 TLS 后的内部转发链路
if (_TLS) {
_SERVE = std::make_unique<HttpServer> (_ADDR, _PORT, "full_chain.crt", "server.key", 4);
} else {
_SERVE = std::make_unique<HttpServer> (_ADDR, _PORT, 4);
}
_KEYGEN = std::make_unique<MyRSA::Generator> ();
_SQLDB = std::make_unique<Sqlite3> (_DB_PATH);
+15
View File
@@ -0,0 +1,15 @@
# 编译产物
waf.bin
*.exe
# 证书与密钥(不入库)
certs/
# OWASP CRS(按需 clone,不入库)
rules/crs/
# 本地覆盖配置
config.local.yaml
# Go
vendor/
+172
View File
@@ -0,0 +1,172 @@
# waf — Web 应用防火墙网关
基于 Go + [coraza](https://github.com/corazawaf/coraza) 的 WAF 网关,部署在 `gateway` 之前,终止 TLS 并按 SecLang 规则库匹配请求,未命中则转发到 gateway。
```
客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则匹配) ──明文 HTTP──▶ gateway:8443 (鉴权 + 反代)
```
## 为什么是 Go
- `coraza-waf/coraza` 是 ModSecurity SecLang 的 Go 实现,100% 兼容 OWASP CRS v4
- `net/http/httputil.ReverseProxy` 标准库反向代理,零第三方依赖
- goroutine + net/http 吞吐量比 Python 高一个数量级
- `go build` 出单二进制,`core/init.txt` 直接拉起
## 极客友好的规则系统
| 操作 | 命令 |
|------|------|
| 编辑规则 | `vim rules/local/custom.conf` |
| 校验语法 | `./waf.bin -test rules/local/custom.conf` |
| 列出所有规则 | `./waf.bin -list` |
| 测试输入 | `./waf.bin -check "/?id=1' OR '1'='1"` |
| 热加载(不重启) | `kill -HUP $(pgrep waf)` |
| 临时禁用某文件 | `mv rules/local/x.conf x.conf.bak && kill -HUP $(pgrep waf)` |
### 规则文件组织
```
rules/
├── waf.conf # 主入口,Include 各子文件
├── local/ # 本地自定义规则(ID 段 100000-199999)
│ ├── custom.conf # 示例:SQL注入/XSS/路径遍历/命令注入/扫描器UA
│ └── exceptions.conf # 误报白名单(SecRuleRemoveById / ctl:ruleRemoveById)
└── crs/ # OWASP CRS(git clone,不入库,ID 段 900000-999999)
```
### 规则 ID 分段
- `100000-199999` 本地自定义
- `900000-999999` OWASP CRS
### 规则语法(SecLang)
```apache
SecRule REQUEST_URI|ARGS "@rx (?i)union[\s/*]+select" \
"id:100001,phase:1,deny,status:403,log,msg:'SQL injection: UNION SELECT'"
```
字段说明:
- `REQUEST_URI|ARGS` 匹配目标(URI / 参数 / Header / Cookie 等)
- `@rx ...` 正则匹配(也支持 `@pm`、`@contains`、`@beginsWith` 等)
- `phase:1` 请求头阶段 / `phase:2` 请求体阶段 / `phase:3` 响应头 / `phase:4` 响应体
- `deny,status:403` 命中后动作
- `msg` 命中日志描述
## 部署形态
WAF 必须终止 TLS 才能看到明文 body,因此证书需从 gateway 迁到 WAF:
1. 把 `gateway` 的 `full_chain.crt` / `server.key` 复制到 `waf/certs/`
2. `gateway` 改为监听 `127.0.0.1:8443`,关闭 TLS(仅本机)
3. `waf` 监听 `:443`,转发到 `http://127.0.0.1:8443`
## 配置
`config.yaml`:
```yaml
listen: ":443"
tls_cert: "./certs/full_chain.crt"
tls_key: "./certs/server.key"
upstream: "http://127.0.0.1:8443"
rules_dir: "./rules"
```
## 目录结构
```
waf/
├── main.go # 入口:服务模式 + CLI 子命令
├── config.yaml # 配置
├── go.mod
├── internal/
│ ├── engine/
│ │ └── engine.go # coraza 封装:加载/热加载/列举/单测
│ └── proxy/
│ └── proxy.go # ReverseProxy + WAF 事务接入
├── rules/
│ ├── waf.conf
│ └── local/
│ ├── custom.conf
│ └── exceptions.conf
├── _set.sh # 编译
├── _run.sh # 运行
├── .gitignore
└── README.md
```
## 安装
```bash
# 1. 安装 Go 1.21+
brew install go # macOS
# 或 sudo apt install golang-go # Debian/Ubuntu
# 2. 编译
bash _set.sh
# 3. 准备证书
mkdir certs && cp /path/to/full_chain.crt /path/to/server.key certs/
# 4. 运行
bash _run.sh
```
## 接入 core 自启动
在 `core/init.txt` 增加:
```
bash /opt/micro_servers/waf/_run.sh
```
并把 `gateway` 改为内部 HTTP 端口(不再监听 443)。
## 命中日志
命中规则时输出 JSON 一行,便于 `jq` 分析:
```json
{"ts":"2026-07-25T10:00:00Z","action":"block","rule_id":100001,"ip":"1.2.3.4","method":"GET","path":"/","query":"id=1'+OR+'1'='1","ua":"curl/8.0","cost_ms":0}
```
聚合统计:
```bash
journalctl -u Nidus -o cat | grep '"action":"block"' | \
jq -r '.rule_id' | sort | uniq -c | sort -rn
```
## 启用 OWASP CRS(可选)
```bash
cd rules
git clone --depth 1 https://github.com/coreruleset/coreruleset.git crs
```
在 `rules/waf.conf` 取消注释:
```apache
Include crs/coraza.conf
Include crs/rules/*.conf
```
然后 `kill -HUP $(pgrep waf)`。
## 已知限制
- 请求体全量读入内存检测,10MB 上限(`SecRequestBodyLimit`)。大文件上传场景需调大或加白名单。
- 响应体检测未启用(需缓冲整个响应,延迟成本高)。
- 限流/黑白名单未实现,可后续作为 coraza 规则或独立中间件加。
## 依赖
- Go 1.21+
- github.com/corazawaf/coraza/v3 v3.7.0
- gopkg.in/yaml.v3
## 许可证
MIT License
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# 运行 WAF(需先 _set.sh 编译)
cd "$(dirname "$0")" || exit
exec ./waf.bin -c config.yaml
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# 构建 WAF 单二进制
set -e
cd "$(dirname "$0")" || exit
if ! command -v go >/dev/null 2>&1; then
echo "[ERROR] 未找到 go,请先安装 Go 1.21+"
exit 1
fi
echo "[1/2] 下载依赖"
go mod download
echo "[2/2] 编译"
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o waf.bin .
echo "完成: $(pwd)/waf.bin"
+10
View File
@@ -0,0 +1,10 @@
# WAF 配置文件
listen: ":443"
tls_cert: "./certs/full_chain.crt"
tls_key: "./certs/server.key"
# gateway 内部 HTTP 端口(gateway 需改为监听 127.0.0.1:8443 不启用 TLS)
upstream: "http://127.0.0.1:8443"
# 规则根目录,主入口为 <rules_dir>/waf.conf
rules_dir: "./rules"
+29
View File
@@ -0,0 +1,29 @@
module github.com/micro_servers/waf
go 1.25.0
require (
github.com/corazawaf/coraza/v3 v3.7.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/corazawaf/libinjection-go v0.3.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect
github.com/kaptinlin/go-i18n v0.1.4 // indirect
github.com/kaptinlin/jsonschema v0.4.6 // indirect
github.com/magefile/mage v1.17.0 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
rsc.io/binaryregexp v0.2.0 // indirect
)
+67
View File
@@ -0,0 +1,67 @@
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=
github.com/corazawaf/coraza/v3 v3.7.0 h1:LIQqu1r+l6e/U/gyiZeykWaNNBY1TzRLz+aaI+QYEEM=
github.com/corazawaf/coraza/v3 v3.7.0/go.mod h1:dOSt5evqC7EstouEv6ghhui01+oVUwp9X1vybWwqTlo=
github.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI=
github.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs=
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY=
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 h1:c7gcNWTSr1gtLp6PyYi3wzvFCEcHJ4YRobDgqmIgf7Q=
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092/go.mod h1:ZZAN4fkkful3l1lpJwF8JbW41ZiG9TwJ2ZlqzQovBNU=
github.com/jcchavezs/mergefs v0.1.1 h1:D45R17m6dHnSVZefnhynoeZvcK2Uw0oTrRfoUOQ0S5Y=
github.com/jcchavezs/mergefs v0.1.1/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk=
github.com/kaptinlin/go-i18n v0.1.4 h1:wCiwAn1LOcvymvWIVAM4m5dUAMiHunTdEubLDk4hTGs=
github.com/kaptinlin/go-i18n v0.1.4/go.mod h1:g1fn1GvTgT4CiLE8/fFE1hboHWJ6erivrDpiDtCcFKg=
github.com/kaptinlin/jsonschema v0.4.6 h1:vOSFg5tjmfkOdKg+D6Oo4fVOM/pActWu/ntkPsI1T64=
github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0=
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
github.com/magefile/mage v1.17.0/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+264
View File
@@ -0,0 +1,264 @@
// Package engine 封装 coraza WAF,提供规则加载、热加载、规则列举与单条输入检测。
package engine
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/types"
)
// Engine 持有当前生效的 WAF 实例,支持通过 Reload 热替换。
type Engine struct {
mu sync.RWMutex
waf coraza.WAF
cfg *Config
}
// Config 引擎配置。
type Config struct {
// RulesDir 规则根目录,主入口文件为 <RulesDir>/waf.conf
RulesDir string
// ErrorCallback 规则加载/解析错误回调
ErrorCallback func(mr types.MatchedRule)
}
// New 创建引擎并首次加载规则。
func New(cfg *Config) (*Engine, error) {
e := &Engine{cfg: cfg}
if err := e.Reload(); err != nil {
return nil, err
}
return e, nil
}
// Reload 重新加载规则文件,原子替换内部 WAF 实例。
// 加载顺序:
// 1. <RulesDir>/waf.conf 基础配置
// 2. <RulesDir>/local/*.conf 本地自定义规则(按文件名字母序)
// 3. <RulesDir>/crs/coraza.conf OWASP CRS 入口(若存在)
// 4. <RulesDir>/crs/rules/*.conf OWASP CRS 规则(按文件名字母序)
//
// 不使用 coraza 的 Include 指令(其路径相对于 cwd 解析易出错),
// 改为 engine 自己扫描目录逐个 WithDirectivesFromFile 加载。
// 失败时保留旧实例不动,确保服务可用。
func (e *Engine) Reload() error {
mainFile := filepath.Join(e.cfg.RulesDir, "waf.conf")
if _, err := os.Stat(mainFile); err != nil {
return fmt.Errorf("主规则文件不存在: %w", err)
}
builder := coraza.NewWAFConfig()
if e.cfg.ErrorCallback != nil {
builder = builder.WithErrorCallback(e.cfg.ErrorCallback)
}
// 1. 主配置
builder = builder.WithDirectivesFromFile(mainFile)
// 2. 本地规则(local/*.conf,按字母序)
localFiles, _ := filepath.Glob(filepath.Join(e.cfg.RulesDir, "local", "*.conf"))
for _, f := range localFiles {
builder = builder.WithDirectivesFromFile(f)
}
// 3. OWASP CRS(可选,crs/coraza.conf + crs/rules/*.conf)
crsEntry := filepath.Join(e.cfg.RulesDir, "crs", "coraza.conf")
if _, err := os.Stat(crsEntry); err == nil {
builder = builder.WithDirectivesFromFile(crsEntry)
crsRuleFiles, _ := filepath.Glob(filepath.Join(e.cfg.RulesDir, "crs", "rules", "*.conf"))
for _, f := range crsRuleFiles {
builder = builder.WithDirectivesFromFile(f)
}
}
newWAF, err := coraza.NewWAF(builder)
if err != nil {
return fmt.Errorf("规则加载失败: %w", err)
}
e.mu.Lock()
e.waf = newWAF
e.mu.Unlock()
return nil
}
// NewTransaction 创建一个事务,由调用方 Close。
func (e *Engine) NewTransaction() types.Transaction {
e.mu.RLock()
defer e.mu.RUnlock()
return e.waf.NewTransaction()
}
// CheckInput 用当前规则测试单个输入字符串,返回命中的规则 ID 列表。
// 极客 CLI 用:waf -check "/?id=1' UNION SELECT * from users"
// 输入按原始未编码字符串处理,内部对 query 做 URL 编码以符合 coraza ParseRequestURI 要求。
func (e *Engine) CheckInput(input string) []int {
tx := e.NewTransaction()
defer tx.Close()
uri := input
if !strings.HasPrefix(uri, "/") {
uri = "/" + uri
}
// 分离 path 与 query,对 query 做合法 URL 编码(空格等非法字符需转义)
// 这样 coraza ProcessURI 内部的 url.ParseRequestURI 不会失败,ARGS 才能被正确提取
fullURI := uri
if idx := strings.Index(uri, "?"); idx >= 0 {
path := uri[:idx]
rawQuery := uri[idx+1:]
// 用 net/url 编码 query:保留 & = ,其他特殊字符转义
var parts []string
for _, kv := range strings.Split(rawQuery, "&") {
eq := strings.Index(kv, "=")
if eq < 0 {
parts = append(parts, url.QueryEscape(kv))
} else {
parts = append(parts, url.QueryEscape(kv[:eq])+"="+url.QueryEscape(kv[eq+1:]))
}
}
fullURI = path + "?" + strings.Join(parts, "&")
}
// 照搬 coraza/http/middleware.go 的 processRequest 流程
tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345)
tx.ProcessURI(fullURI, "GET", "HTTP/1.1")
tx.AddRequestHeader("Host", "localhost")
tx.SetServerName("localhost")
if it := tx.ProcessRequestHeaders(); it != nil {
return []int{it.RuleID}
}
if it, _ := tx.ProcessRequestBody(); it != nil {
return []int{it.RuleID}
}
return nil
}
// ListRules 扫描规则目录,返回所有 SecRule 的 ID 与原始行。
// 仅做文本扫描,不解析语义——极客用 grep 友好。
// 处理 `\` 续行:以 `\` 结尾的行与下一行合并后再抽取 id。
func (e *Engine) ListRules() ([]Rule, error) {
var rules []Rule
err := filepath.Walk(e.cfg.RulesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
if !strings.HasSuffix(path, ".conf") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
rel, _ := filepath.Rel(e.cfg.RulesDir, path)
// 先合并续行,保留每条逻辑行在原文件的起始行号
type logicalLine struct {
startLine int
text string
}
var lines []logicalLine
rawLines := strings.Split(string(data), "\n")
for i := 0; i < len(rawLines); i++ {
start := i + 1
cur := strings.TrimRight(rawLines[i], "\r")
// 合并以 \ 结尾的续行
for strings.HasSuffix(cur, "\\") && i+1 < len(rawLines) {
cur = cur[:len(cur)-1] + " " + strings.TrimRight(rawLines[i+1], "\r")
i++
}
lines = append(lines, logicalLine{startLine: start, text: cur})
}
for _, ll := range lines {
line := strings.TrimSpace(ll.text)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if id, ok := extractRuleID(line); ok {
rules = append(rules, Rule{
ID: id,
File: rel,
Line: ll.startLine,
Source: line,
})
}
}
return nil
})
return rules, err
}
// Rule 描述一条 SecRule 的位置信息。
type Rule struct {
ID int `json:"id"`
File string `json:"file"`
Line int `json:"line"`
Source string `json:"source"`
}
// extractRuleID 从一行 SecRule 中抠出 id:nnnn 字段。
// 形如: SecRule REQUEST_URI "@rx (?i)union.*select" "id:100001,phase:1,deny"
func extractRuleID(line string) (int, bool) {
idx := strings.Index(line, "id:")
if idx < 0 {
return 0, false
}
rest := line[idx+3:]
end := 0
for end < len(rest) {
c := rest[end]
if c < '0' || c > '9' {
break
}
end++
}
if end == 0 {
return 0, false
}
var id int
fmt.Sscanf(rest[:end], "%d", &id)
return id, true
}
// TestFile 仅尝试加载单个规则文件,验证语法。
// 通过临时拼一个 Include 实现,目标文件转绝对路径以适配 coraza 解析目录。
func TestFile(file string) error {
abs, err := filepath.Abs(file)
if err != nil {
return err
}
if _, err := os.Stat(abs); err != nil {
return fmt.Errorf("文件不存在: %w", err)
}
tmp, err := os.CreateTemp("", "waf-test-*.conf")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
_, err = tmp.WriteString(fmt.Sprintf("Include %s\n", abs))
tmp.Close()
if err != nil {
return err
}
builder := coraza.NewWAFConfig().WithDirectivesFromFile(tmp.Name())
_, err = coraza.NewWAF(builder)
if err != nil {
return fmt.Errorf("语法错误: %w", err)
}
return nil
}
// LogError 默认错误回调,输出到 stderr。
func LogError(mr types.MatchedRule) {
fmt.Fprintf(os.Stderr, "[coraza] rule_id=%d severity=%s msg=%s\n",
mr.Rule().ID(), mr.Rule().Severity(), mr.ErrorLog())
}
+156
View File
@@ -0,0 +1,156 @@
// Package proxy 封装到 gateway 的反向代理,并在请求/响应阶段接入 WAF 事务。
package proxy
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/micro_servers/waf/internal/engine"
)
// Proxy 持有 ReverseProxy 与 WAF 引擎引用。
type Proxy struct {
rp *httputil.ReverseProxy
waf *engine.Engine
}
// New 创建代理。upstream 形如 "http://127.0.0.1:8443"。
func New(upstream string, waf *engine.Engine) (*Proxy, error) {
target, err := url.Parse(upstream)
if err != nil {
return nil, err
}
rp := httputil.NewSingleHostReverseProxy(target)
p := &Proxy{rp: rp, waf: waf}
// 自定义 Director:保留原 Host / X-Forwarded-For
origDirector := rp.Director
rp.Director = func(req *http.Request) {
origDirector(req)
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Header.Set("X-Forwarded-Proto", "https")
}
// 透传错误,不暴露内部堆栈
rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("[proxy] upstream_error ip=%s path=%s err=%v", r.RemoteAddr, r.URL.Path, err)
http.Error(w, "upstream unavailable", http.StatusBadGateway)
}
return p, nil
}
// ServeHTTP 实现 http.Handler。
// 流程:ProcessRequest → 命中则 403 → 否则转发 → ProcessResponseHeaders。
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
tx := p.waf.NewTransaction()
defer func() {
tx.ProcessLogging()
_ = tx.Close()
}()
// 解析 RemoteAddr 中的 IP 与端口
client := r.RemoteAddr
cport := 0
if idx := strings.LastIndexByte(r.RemoteAddr, ':'); idx != -1 {
client = r.RemoteAddr[:idx]
cport, _ = strconv.Atoi(r.RemoteAddr[idx+1:])
}
// 请求阶段:照搬 coraza/http/middleware.go 的 processRequest 流程
tx.ProcessConnection(client, cport, "", 0)
tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
for k, vs := range r.Header {
for _, v := range vs {
tx.AddRequestHeader(k, v)
}
}
if r.Host != "" {
tx.AddRequestHeader("Host", r.Host)
tx.SetServerName(r.Host)
}
if it := tx.ProcessRequestHeaders(); it != nil {
p.block(w, r, it.RuleID, start)
return
}
// 请求体:读全量喂给 coraza,再用 buffer 恢复 r.Body 给 proxy 转发
if r.Body != nil && r.Body != http.NoBody {
bodyBytes, err := io.ReadAll(r.Body)
if err == nil {
_, _, _ = tx.ReadRequestBodyFrom(bytes.NewReader(bodyBytes))
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
if it, _ := tx.ProcessRequestBody(); it != nil {
p.block(w, r, it.RuleID, start)
return
}
}
// 包装 ResponseWriter 以捕获状态码与响应头
rw := &responseRecorder{ResponseWriter: w, status: 200}
p.rp.ServeHTTP(rw, r)
// 响应阶段:把 upstream 的 header 喂给 WAF
for k, vs := range rw.headers {
for _, v := range vs {
tx.AddResponseHeader(k, v)
}
}
tx.ProcessResponseHeaders(rw.status, "HTTP/1.1")
log.Printf("[proxy] pass ip=%s method=%s path=%s status=%d rule=- cost=%s",
r.RemoteAddr, r.Method, r.URL.Path, rw.status, time.Since(start))
}
// block 命中规则时统一返回 403,并打 JSON 日志便于 jq 分析。
func (p *Proxy) block(w http.ResponseWriter, r *http.Request, ruleID int, start time.Time) {
rec := map[string]any{
"ts": time.Now().UTC().Format(time.RFC3339),
"action": "block",
"rule_id": ruleID,
"ip": r.RemoteAddr,
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"ua": r.UserAgent(),
"cost_ms": time.Since(start).Milliseconds(),
}
if raw, err := json.Marshal(rec); err == nil {
log.Println(string(raw))
}
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("blocked by WAF\n"))
}
// responseRecorder 记录状态码与响应头,供响应阶段 WAF 检测使用。
type responseRecorder struct {
http.ResponseWriter
status int
headers http.Header
}
func (r *responseRecorder) WriteHeader(code int) {
r.status = code
if r.headers == nil {
r.headers = http.Header{}
}
// 拷贝上游响应头
for k, vs := range r.Header() {
r.headers[k] = vs
}
r.ResponseWriter.WriteHeader(code)
}
func (r *responseRecorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
+141
View File
@@ -0,0 +1,141 @@
// Command waf 是 micro_servers 的 WAF 入口网关。
//
// 部署形态:客户端 ──HTTPS──▶ waf:443 ──明文 HTTP──▶ gateway:内部端口
// WAF 终止 TLS、运行 coraza 规则匹配,未命中则转发到 gateway。
//
// 极客操作:
//
// kill -HUP <pid> # 热加载规则
// waf -list # 列出所有规则
// waf -test rules/local/x.conf # 校验单文件语法
// waf -check "/?id=1' OR '1'='1" # 用当前规则测试输入
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"gopkg.in/yaml.v3"
"github.com/micro_servers/waf/internal/engine"
"github.com/micro_servers/waf/internal/proxy"
)
// Config WAF 配置文件结构。
type Config struct {
Listen string `yaml:"listen"` // 监听地址,默认 :443
TLSCert string `yaml:"tls_cert"` // 证书文件
TLSKey string `yaml:"tls_key"` // 私钥文件
Upstream string `yaml:"upstream"` // gateway 地址,如 http://127.0.0.1:8443
RulesDir string `yaml:"rules_dir"` // 规则根目录
}
func main() {
var (
configFile = flag.String("c", "config.yaml", "配置文件路径")
doList = flag.Bool("list", false, "列出所有规则后退出")
doTest = flag.String("test", "", "校验单个规则文件语法后退出")
doCheck = flag.String("check", "", "用当前规则测试输入字符串(URI/path)后退出")
)
flag.Parse()
cfg := loadConfig(*configFile)
// 子命令:极客 CLI 工具
if *doTest != "" {
if err := engine.TestFile(*doTest); err != nil {
fmt.Fprintln(os.Stderr, "FAIL:", err)
os.Exit(1)
}
fmt.Println("OK")
return
}
eng, err := engine.New(&engine.Config{
RulesDir: cfg.RulesDir,
ErrorCallback: engine.LogError,
})
if err != nil {
log.Fatalf("引擎初始化失败: %v", err)
}
if *doList {
rules, err := eng.ListRules()
if err != nil {
log.Fatal(err)
}
out, _ := json.MarshalIndent(rules, "", " ")
fmt.Println(string(out))
return
}
if *doCheck != "" {
ids := eng.CheckInput(*doCheck)
fmt.Printf("input=%q hit_rules=%v\n", *doCheck, ids)
return
}
// 服务模式
p, err := proxy.New(cfg.Upstream, eng)
if err != nil {
log.Fatalf("代理初始化失败: %v", err)
}
// SIGHUP 热加载
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
go func() {
for range sigCh {
log.Println("[reload] 收到 SIGHUP,重新加载规则")
if err := eng.Reload(); err != nil {
log.Printf("[reload] 失败,保留旧规则: %v", err)
} else {
log.Println("[reload] 成功")
}
}
}()
server := &http.Server{
Addr: cfg.Listen,
Handler: p,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
log.Printf("waf listening on %s, upstream=%s, rules=%s",
cfg.Listen, cfg.Upstream, cfg.RulesDir)
log.Printf(" - 热加载: kill -HUP %d", os.Getpid())
log.Printf(" - 列规则: %s -c %s -list", os.Args[0], *configFile)
if err := server.ListenAndServeTLS(cfg.TLSCert, cfg.TLSKey); err != nil {
log.Fatal(err)
}
}
func loadConfig(path string) *Config {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("读取配置失败: %v", err)
}
var c Config
if err := yaml.Unmarshal(data, &c); err != nil {
log.Fatalf("解析配置失败: %v", err)
}
if c.Listen == "" {
c.Listen = ":443"
}
if c.RulesDir == "" {
c.RulesDir = "./rules"
}
if c.Upstream == "" {
log.Fatal("upstream 不能为空")
}
return &c
}
+26
View File
@@ -0,0 +1,26 @@
# custom.conf —— 本地自定义规则示例
# ID 段:100000-199999
# 100001: 拦截 SQL 注入特征(UNION SELECT)
SecRule REQUEST_URI|ARGS "@rx (?i)union[\s/*]+select" \
"id:100001,phase:1,deny,status:403,log,msg:'SQL injection: UNION SELECT'"
# 100002: 拦截 SQL 注释符
SecRule ARGS "@rx (?i)(--|#|/\*.*\*/)" \
"id:100002,phase:2,deny,status:403,log,msg:'SQL injection: comment'"
# 100003: 拦截 XSS 标签
SecRule ARGS|REQUEST_HEADERS "@rx (?i)<script|javascript:|onerror=|onload=" \
"id:100003,phase:2,deny,status:403,log,msg:'XSS probe'"
# 100004: 路径遍历
SecRule REQUEST_URI|ARGS "@rx (?i)\.\./|\.\.%2f|\.\.%5c" \
"id:100004,phase:1,deny,status:403,log,msg:'Path traversal'"
# 100005: 命令注入常见关键字
SecRule ARGS "@rx (?i)(;|\|)&?\s*(cat|ls|id|whoami|uname|wget|curl|bash|sh)\s" \
"id:100005,phase:2,deny,status:403,log,msg:'Command injection'"
# 100006: 已知恶意扫描器 UA
SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(nikto|sqlmap|nmap|masscan|acunetix|nessus)" \
"id:100006,phase:1,deny,status:403,log,msg:'Malicious scanner UA'"
+6
View File
@@ -0,0 +1,6 @@
# exceptions.conf —— 误报白名单
# 当某条 CRS 规则误报时,在此用 SecRuleRemoveById 或 ctl:ruleRemoveById 关闭
#
# 示例:关闭 100003 对 /login 路径的检查
# SecRule REQUEST_URI "@beginsWith /login" \
# "id:100100,phase:1,pass,nolog,ctl:ruleRemoveById=100003"
+22
View File
@@ -0,0 +1,22 @@
# waf.conf —— WAF 规则主入口(仅放基础配置)
# 极客操作:
# - 在 local/ 下新增 *.conf 文件,engine 会自动扫描加载(按文件名字母序)
# - 修改后执行 `kill -HUP <pid>` 热加载
# - 校验语法: `waf -test rules/local/xxx.conf`
#
# 规则 ID 分段约定:
# 100000-199999 本地自定义规则
# 900000-999999 OWASP CRS(由 crs/ 提供,不手改)
#
# 注意:本文件不用 Include 指令(coraza 的 Include 路径相对于 cwd 解析,
# 易出错)。engine.Reload 会先加载本文件,再扫描 local/*.conf 逐个加载。
# ===== 基础配置(SecLang) =====
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecDefaultAction "phase:1,pass,log,auditlog"
# 请求体限制 10MB
SecRequestBodyLimit 10485760