Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenLiteWaf

运行在 nginx 容器内的 OpenResty Lua WAF。在反向代理层对请求做 CC 限速与攻击特征检查,并维护一个公开的统计页。它工作在 nginx access 阶段,所有状态保存在 lua_shared_dict 中,并定时快照到挂载目录实现持久化(进程重启自动恢复,最多丢失最近一个快照间隔的数据;目录不可写时退化为内存模式)。许可证:MIT。

代码结构:

OpenLiteWaf/
├── lua/
│   ├── openlitewaf.lua   # 核心模块:配置、规则、检查逻辑、统计输出
│   ├── waf.lua           # access_by_lua_file 入口,调用 openlitewaf.access()
│   └── stats.lua         # content_by_lua_file 入口,调用 openlitewaf.stats()
├── nginx/nginx.conf      # OpenResty 主配置(lua_package_path、shared dict、init)
└── tests/                # 回归测试,见"规则维护"

工作方式

每个请求进入 access 阶段后按固定顺序检查,任一步命中即终止。ACME 白名单路径(/.well-known/acme-challenge/)直接放行;已在封禁名单中的 IP 返回 403;CC 计数超过窗口阈值时封禁该 IP 并返回 403;统计页自身的三个 URI(/security/security/stats/security/logs)跳过特征检查;其余请求进入特征匹配。

特征匹配的对象依次为:原始 request_uri(保留编码形态,用于捕获 %2e%2e%2f 一类编码特征)、规范化路径、User-Agent、完整 URL 解码后的 request_uri(含 query)、请求体、请求体的一次 URL 解码。URL 解码只做一层:两层编码能被解码后的检查对象覆盖,三层编码则绕过。规则表按定义顺序匹配,命中即停,命中的类目决定计数归属与警告页显示的类目。

请求体检查只对 POST/PUT/PATCH 生效,且要求 Content-Length 不超过 2MB,扫描范围为前 64KB——body 超出 nginx client_body_buffer_size 落盘时读文件头部。payload 置于 64KB 之后或拆入超限请求可以绕过,这是内存与 IO 成本上的取舍。日志内容类端点(见 body_exempt_prefixes,默认豁免 /v1/log/1/log)整体豁免 body 检查:日志原文中出现攻击特征属于正常业务,做扫描会误伤;若新增接收任意文本的端点,应将其加入该列表。

命中特征后,模块封禁该 IP(默认 600 秒)、按类目计数、写入攻击日志与分钟趋势,并返回 403 页面(卡片组件,无页面外壳,支持 iframe 嵌入;拦截原因仅写入 nginx error log,不在页面上展示)。

统计页

GET /security 返回 HTML 页面,由内联 JS 渲染并每 30 秒轮询刷新,需要浏览器启用 JavaScript。数据接口有两个 JSON 端点:

GET /security/stats 返回汇总:累计请求与拦截数(requests_totalblocked_total)、最近 60 分钟拦截数(blocked_60m,为 trends 各分钟桶之和,不含封禁期内无类目的拦截)、按类目分布(blocked,含 ccsqlixsstraversalrceprobe)、当前封禁 IP 数(banned_active)、缓存日志条数(logs_total)、最近 60 分钟逐分钟拦截数(trends)与来源 IP Top(top_ips,按最近日志聚合)。

GET /security/logs?page=N 返回攻击日志分页,每页 50 条,最新在前,页码越界时收敛到边界页。每条记录包含时间(t)、类目(cat)、脱敏 IP(ip)、URI(u)与 User-Agent(a)。

公开页面的隐私处理:IP 一律脱敏(IPv4 保留前两段,IPv6 保留前三组),URI 中的 token= 参数值替换为 ***,完整 IP 只出现在 nginx error log 的拦截记录中。统计页是公开端点,攻击者同样可以访问,它受 CC 限流保护。

数据以环形槽位保存在 shared dict:攻击日志 500 条、封禁槽位 1024 个,写满后覆盖最旧记录。shared dict 不支持枚举键,banned_active 通过遍历封禁槽位统计未到期数量得出,同一 IP 重复封禁会覆盖槽位,结果可能低估。

持久化:worker 0 每 60 秒将计数、趋势分钟桶、攻击日志环形缓冲与未到期封禁名单(含封禁 IP 登记环 br:*)写为 /data/openlitewaf/snapshot.json(临时文件 + rename 原子替换),init_by_lua 阶段恢复;封禁按剩余 TTL 重建。快照间隔内的数据变更在进程崩溃时丢失。/data/openlitewaf 须以读写挂载,目录不可写时退化为内存模式。

部署

nginx 服务使用 OpenResty 镜像(当前适配 openresty/openresty:1.27.1.2-alpine),相关挂载:

volumes:
  - ./nginx:/etc/nginx/conf.d:ro                                    # 站点配置
  - ../OpenLiteWaf/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
  - ../OpenLiteWaf/lua:/usr/local/openresty/nginx/lua:ro
  - ../OpenLiteWaf/data:/data/openlitewaf                           # 快照持久化目录(读写)

站点配置的 80 与 443 两个 server 都需要 access_by_lua_file,443 另有三个统计 location:

access_by_lua_file /usr/local/openresty/nginx/lua/waf.lua;
location = /security       { content_by_lua_file /usr/local/openresty/nginx/lua/stats.lua; }
location = /security/stats { content_by_lua_file /usr/local/openresty/nginx/lua/stats.lua; }
location = /security/logs  { content_by_lua_file /usr/local/openresty/nginx/lua/stats.lua; }

独立部署时需在 http 级提供 lua_package_pathlua_shared_dict openlitewaf 16minit_by_lua_block(见 nginx/nginx.conf),再挂接上述 location。

改动 Lua 文件后需要让 OpenResty 重新加载:nginx -s reload 或容器 restart。OpenResty 默认开启 lua_code_cachegit pull 只更新磁盘文件,不重载则 worker 仍运行旧代码。reload 保留计数,重启清零。

升级 OpenResty 镜像 tag 时,核对 nginx/nginx.conf 与镜像内置配置的差异(lua 指令、临时路径、默认 include),必要时重新挂载。

配置

全部配置集中在 lua/openlitewaf.lua 顶部的 CONFIG 表:

配置 默认值 说明
cc.window / cc.limit 10 / 240 CC 窗口秒数 / 窗口内请求上限
cc.ban 600 CC 封禁秒数
sig_ban 600 命中特征后的封禁秒数
whitelist_prefixes /.well-known/acme-challenge/ 完全白名单,不计数不检查
body_exempt_prefixes /v1/log/1/log 请求体检查豁免前缀
body_scan_limit / body_size_limit 65536 / 2MB body 扫描前 N 字节 / 超过该 Content-Length 跳过
log_capacity / log_page_size 500 / 50 攻击日志环形容量 / 每页条数
ban_slots 1024 封禁槽位数
trend_minutes 60 趋势图统计的分钟数
top_ips 8 统计页展示的来源 IP Top 数
log_field_max 160 攻击日志单字段(URI / UA)最大长度
stats_prefix /security 统计页前缀
dict_name openlitewaf 须与 lua_shared_dict 名一致

CC 阈值与 nginx 静态限速有联动关系:limit_req(PREACCESS 阶段)先于本模块(ACCESS 阶段)执行,超额请求以 503 丢弃、不进入计数,因此 cc.limit 折算(limit / window)必须低于 limit_req 速率,否则 CC 封禁不会触发。调整任一侧时检查另一侧。短时高频脉冲会被 limit_req 直接丢弃,不会计入本模块。

规则

规则定义在 lua/openlitewaf.lua_M.RULES,位于 -- RULES-BEGIN / -- RULES-END 标记之间,每条为 { "类目", [[PCRE]] },类目为小写字母:sqlixsstraversalrceprobe。正则使用 ngx.re 语法,编译 flags 为 ji(忽略大小写 + JIT)。

编写时注意:规则必须保持单行双元素表,tests/openlitewaf_regex_test.phpRULES-BEGIN/END 标记与该格式解析;规则内容含 ]] 时(如字符类 [a-z] 结尾),Lua 长括号改用 [==[ ]==];规则按顺序匹配、命中即停,新增规则的位置影响计数归属;修改或新增规则时,同步在 tests/openlitewaf_regex_test.php 补充正样本(应拦截)与反样本(不应拦截)。

回归测试与语法检查:

php OpenLiteWaf/tests/openlitewaf_regex_test.php    # 规则正负样本(PHP PCRE,与 ngx.re 同源)
lua5.1 OpenLiteWaf/tests/openlitewaf_logic_test.lua # 检查流程/CC/体检查/日志/统计逻辑(stub ngx)
luac5.1 -p OpenLiteWaf/lua/*.lua                    # 语法检查

注意事项

规则只覆盖已知特征,语义层攻击与定向利用不在防护范围。限速与日志按 IP 维度统计,NAT 与共享出口会造成误伤;nginx 前存在 CDN 或负载均衡时,remote_addr 是节点 IP,需先配置 nginx real_ip 模块并只信任已知代理,否则 CC 计数与日志 IP 均不准确。本模块无 IP 信誉库、无 JS 挑战、无 GeoIP 定位(趋势图为本站数据),高强度抗 CC 依赖前置 CDN/WAF。

许可证

MIT,见 LICENSE

About

OpenLiteWaf — 极简 OpenResty Lua WAF:CC 限速、攻击特征拦截、公开统计页。MIT License

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages