lua-resty-waf
基于 OpenResty 的 Lua 编写的简单 WAF
$ opm get Kiuber/lua-resty-waf
项目说明
0. 安装使用
本项目 fork 自 codiy1992/lua-resty-waf,感谢 codiy1992
本项目基于 OpenResty,所以需要先安装好 OpenResty, Linux 各发行版安装详见 OpenResty® Linux 包
通过 OpenResty 的包管理器
opm
安装本项目opm get Kiuber/lua-resty-waf
如下配置 nginx,即可正常工作
http {
# 在 http 区块添加如下设定
lua_code_cache on;
lua_need_request_body on;
lua_shared_dict waf 32k;
lua_shared_dict list 10m;
lua_shared_dict limiter 10m;
lua_shared_dict counter 10m;
lua_shared_dict sampler 10m;
init_worker_by_lua_block {
if ngx.worker.id() == 0 then
ngx.timer.at(0, require("resty.waf").init)
end
}
access_by_lua_block {
local waf = require("resty.waf")
waf.run({
"manager",
"filter",
"limiter",
"counter",
"sampler",
}, "/waf")
}
}
通过 WAF_CONFIG_PROVIDER 环境变量设置配置信息来源
如果未设置则使用
config.lua
参数如果设置为
redis
,则从 redis 中获取配置信息并覆盖config.lua
中的参数如果设置为
local_file
,则从/data/waf-config.json
中获取配置并作为config
WAF_CONFIG_PROVIDER=(redis || local_file)
1. 几个共享内存
当可用内存不足时, 将自动覆盖最久未被使用的未过期 key
lua_shared_dict waf 32k;
存放 waf 配置等信息lua_shared_dict list 10m;
存放 ip/device/uid 名单, 用于提供matcher
之外的匹配功能lua_shared_dict limiter 10m;
存放请求频率限制信息lua_shared_dict counter 10m;
存放请求次数统计信息lua_shared_dict sampler 10m;
存放采样器的采样信息
2. 执行流程
init_worker_by_lua
阶段, 读入默认配置, 并从 redis 获取最新配置信息, 合并两者放入共享内存access_by_lua
阶段, 从共享内存读取配置, 顺序执行对应模块
3. 配置的结构
配置由三大部分组成如下
matchers
一些匹配规则, 可在各模块间共用, 用于匹配特定请求responses
自定义响应格式, 可在各模块间共用, 用于 waf 模块内的 http 响应modules
模块配置, 包含manager
,filter
,limiter
,counter
,sampler
五大模块
3.1 Matcher
在模块内根据 HTTP 请求的 ip
, uri
, args
, header
, body
, user_agent
, referer
等信息匹配请求, 匹配命中的请求将在模块内进行下一步操作比如, 限制访问直接返回或者记录请求频次等
matcher 里的操作符 (operator)
*
默认返回true
, 即默认匹配=
判断两个值否相等, 字符串将忽略大小写==
判断两个值是否相等, 大小写敏感!=
判断两个值是否不相等≈
判断字符串是否包含于另一字符串中, 或匹配正则!≈
判断字符串是否不包含在另一字符串中, 或不匹配正则#
判断某个值是否出现在table
中Exist
判断某值是否不为nil
!Exist
or!
判断某值是否为nil
以下为内置的默认配置, 可以根据需求使用 redis
或者 /waf/config
接口进行配置
{
"any": {}, // 匹配任意请求, 可以有其他名字, 如 `"*": {}`
"attack_sql": {// 从args中匹配sql注入字符, 默认配置仅提供简单示例, 可以自行增加/修改配置
"Args": {
"name": ".*",
"operator": "≈",
"value": "select.*from"
}
},
"attack_file_ext": {// 匹配URI中以特定字符结尾的请求
"URI": {
"value": "\\.(htaccess|bash_history|ssh|sql)$",
"operator": "≈"
}
},
"attack_agent": { // 匹配特定UserAgent请求
"UserAgent": {
"value": "(nmap|w3af|netsparker|nikto|fimap|wget)",
"operator": "≈"
}
},
"post": {
"Method": {
"value": "(put|post)",
"operator": "≈"
}
},
"trusted_referer": {
"Method": {
"value": {},
"operator": "#"
}
},
"wan": { // 匹配来自公网的请求
"IP": {
"value": "(10.|192.168|172.1[6-9].|172.2[0-9].|172.3[01].).*",
"operator": "!≈"
}
},
"app_id": { // 匹配头信息X-App-ID的值出现在value中的请求
"Header": {
"name": "x-app-id",
"operator": "#",
"value": [
0
]
}
},
"app_version": { // 匹配头信息X-App-Version的值出现在value中的请求
"Header": {
"name": "x-app-version",
"operator": "#",
"value": [
"0.0.0"
]
}
},
"uid": { // 匹配 Authorization Bearer Token 的 sub 字段
"UID": {
"value": [
0
],
"operator": "#"
}
}
}
3.2 Response
用于 waf
模块拒绝请求时候响应给客户端
默认配置如下, 可自行增加或修改配置
{
"403": { // 对于各模块规则中的`code`, 不需要与HTTP的`status code`对应
"status": 403, // HTTP的`status code`
"body": "{\"code\":\"403\", \"message\":\"403 Forbidden\"}",
"mime_type": "application/json"
}
}
3.3 Manager 模块
用于 waf 的管理, 提供一系列以 /waf
开头的路由, 需要通过 Basic Authorizaton 认证 默认账号密码 waf:TTpsXHtI5mwq
或者指定头信息 Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==
可使用项目根目录下的 postman.json
导入 postman 进行使用
| 路由 | METHOD | 用途 | |-|-|-| |/waf/status
| GET | 获取状态信息 | |/waf/config
| GET | 获取当前配置 | |/waf/config
| POST | 临时变更配置| 在 nginx 重启或执行 /waf/config/reload
后失效 | |/waf/config/reload
| POST | 重载配置, 将使 /waf/config
提交的临时配置失效 | |/waf/list
| GET | 查看当前 list
中的名单及其 ttl | |/waf/list
| POST | 临时增加/修改名单, 在 nginx 重启或执行 /waf/list/reload
后失效 | |/waf/list/reload
| POST | 重载名单配置, 将覆盖 /waf/list
提交的临时配置 | |/waf/module/limiter
| GET | 查询请求频次限制器情况 | |/waf/module/counter
| GET | 查询请求计数器统计情况 | |/waf/module/sampler
| GET | 查询采集器里的采样数据 |
3.4 Filter 模块
用于过滤请求, 流程如下
matcher
匹配上的请求, 执行放行accept
或者拒绝block
操作执行
accept
将请求交给下一模块处理执行
block
将根据过滤规则rule
中指定的code
匹配相应response
作为返回
模块默认配置如下
{
"enable": true, // 可配置关闭此模块, 默认开启
"rules": [
{
"action": "block", // accept or block
"matcher": "any", // 详见 matcher 说明
"code": 403, // 执行block时用于匹配对应response
"enable": true, // 规则开关
"by": "ip:in_list" // Optional, 使用在nginx共享内存维护的名单(`list`)来扩展matcher功能
},
{
"action": "block",
"matcher": "any",
"code": 403,
"enable": true,
"by": "device:in_list"
},
{
"action": "block",
"matcher": "any",
"code": 403,
"enable": true,
"by": "uid:in_list"
},
{
"enable": true,
"action": "block",
"matcher": "attack_sql",
"code": 403
},
{
"enable": true,
"action": "block",
"matcher": "attack_file_ext",
"code": 403
},
{
"enable": true,
"action": "block",
"matcher": "attack_agent",
"code": 403
},
{
"enable": false,
"action": "block",
"matcher": "app_id",
"code": 403
},
{
"enable": false,
"action": "block",
"matcher": "app_version",
"code": 403
}
]
}
3.5 Limiter 模块
用于请求频率限制, 对于匹配 matcher
的请求, 可基于 ip
, uri
, uid
, device
及其组合建立频率控制规则
模块默认配置如下
{
"enable": true, // 可配置关闭此模块, 默认开启
"rules": [
{ // 每个IP对所有URI,每分钟至多通过60个请求, 超过则拒绝
"time": 60, // 时间: 单位秒
"code": 403, // 拒绝时用于匹配对应response的响应码
"enable": false, // 默认关闭
"count": 60, // 允许请求数
"matcher": "any",
"by": "ip"
},
{ // 每个IP对单一URI,每分钟至多通过10个请求, 超过则拒绝
"time": 60,
"code": 403,
"enable": false, // 默认关闭
"count": 10,
"matcher": "any",
"by": "ip,uri"
}
]
}
可用接口 /waf/module/limiter
查询此模块信息
curl --location --request GET 'http://127.0.0.1/waf/module/limiter' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==' \
--data-raw '{
"count": 1, // 请求数量 >= 1
"scale": 1024, // 数据规模设置为0可取全部统计数据,默认1024
"q": "", // 查询匹配, 可以是字符串或者正则表达式
"key": "" // 指定要查看的维度(ip, uri, uid, device)
}'
3.6 Counter 模块
统计请求次数, 根据 ip
, uri
, uid
device
及其任意组合如 ip,uri
, uri,ip
, 来统计请求次数
模块默认配置如下
{
"enable": true, // 可配置关闭此模块, 默认开启
"rules": [
{ // 对于任意请求, 按IP统计请求次数, 默认关闭
"enable": false,
"matcher": "any",
"time": 60,
"by": "ip"
},
{// 对于任意请求, 按IP+URI统计请求次数, 默认关闭
"enable": false,
"matcher": "any",
"time": 60,
"by": "ip,uri"
}
]
}
可用接口 /waf/module/limiter
观察统计信息
curl --location --request GET 'http://127.0.0.1/waf/module/counter' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==' \
--data-raw '{
"count": 1, // 请求数量 >= 1
"scale": 1024, // 数据规模设置为0可取全部统计数据,默认1024
"q": "", // 查询匹配, 可以是字符串或者正则表达式
"key": "" // 指定要查看的维度(ip, uri, uid, device)
}'
3.7 Sampler 模块
采样器, 模块支持两个内置的额外 matcher: filtered
, limited
即匹配被过滤或限制的请求, 也可根据其他 matcher 自定义规则.
模块默认配置如下
{
"rules": [
{
"rate": 25, // 采样率,当达集数据集到size时,依据rate以firt-in-first-out规则替换原有数据
"size": 10,
"matcher": "filtered",
"enable": false
},
{
"rate": 25, // 采样率,当达集数据集到size时,依据rate以firt-in-first-out规则替换原有数据
"size": 10,
"matcher": "limited",
"enable": false
}
],
"enable": true
}
使用接口 /waf/module/sampler
获取采样数据
curl --location --request GET '127.0.0.1:8080/waf/module/sampler' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==' \
--data-raw '{
"q": "", // 查询字符串
"all": false, // 是否输出所有采样数据(单一采样规则下的), 默认true
"pop": false // 取出采样时候是否清空采样队列, 默认true
}'
3.8 完整的默认配置
{
"matchers": {
"attack_file_ext": {
"URI": {
"operator": "≈",
"value": "\\.(htaccess|bash_history|ssh|sql)$"
}
},
"app_version": {
"Header": {
"value": [
"0.0.0"
],
"name": "x-app-version",
"operator": "#"
}
},
"app_id": {
"Header": {
"value": [
0
],
"name": "x-app-id",
"operator": "#"
}
},
"trusted_referer": {
"Method": {
"operator": "#",
"value": {}
}
},
"uid": {
"UID": {
"operator": "#",
"value": [
0
]
}
},
"attack_agent": {
"UserAgent": {
"operator": "≈",
"value": "(nmap|w3af|netsparker|nikto|fimap|wget)"
}
},
"any": {},
"attack_sql": {
"Args": {
"value": "select.*from",
"name": ".*",
"operator": "≈"
}
},
"wan": {
"IP": {
"operator": "!≈",
"value": "(10.|192.168|172.1[6-9].|172.2[0-9].|172.3[01].).*"
}
},
"post": {
"Method": {
"operator": "≈",
"value": "(put|post)"
}
}
},
"responses": {
"403": {
"body": "{\"code\":403, \"message\":\"Forbidden\"}",
"mime_type": "application/json",
"status": 403
}
},
"modules": {
"sampler": {
"enable": true,
"rules": [
{
"enable": false,
"rate": 25,
"matcher": "filtered",
"size": 10
},
{
"enable": false,
"rate": 25,
"matcher": "limited",
"size": 10
}
]
},
"manager": {
"auth": {
"pass": "TTpsXHtI5mwq",
"user": "waf"
},
"enable": true
},
"filter": {
"enable": true,
"rules": [
{
"action": "block",
"by": "ip:in_list",
"enable": true,
"matcher": "any",
"code": 403
},
{
"action": "block",
"by": "device:in_list",
"enable": true,
"matcher": "any",
"code": 403
},
{
"action": "block",
"by": "uid:in_list",
"enable": true,
"matcher": "any",
"code": 403
},
{
"enable": true,
"action": "block",
"matcher": "attack_sql",
"code": 403
},
{
"enable": true,
"action": "block",
"matcher": "attack_file_ext",
"code": 403
},
{
"enable": true,
"action": "block",
"matcher": "attack_agent",
"code": 403
},
{
"enable": false,
"action": "block",
"matcher": "app_id",
"code": 403
},
{
"enable": false,
"action": "block",
"matcher": "app_version",
"code": 403
}
]
},
"limiter": {
"enable": true,
"rules": [
{
"count": 60,
"by": "ip",
"enable": false,
"code": 403,
"time": 60,
"matcher": "any"
},
{
"count": 10,
"by": "ip,uri",
"enable": false,
"code": 403,
"time": 60,
"matcher": "any"
}
]
},
"counter": {
"enable": true,
"rules": [
{
"enable": false,
"by": "ip",
"time": 60,
"matcher": "any"
},
{
"enable": false,
"by": "ip,uri",
"time": 60,
"matcher": "any"
}
]
}
}
}
4. 自定义配置(临时生效, 通过 HTTP 接口)
4.1 自定义配置 config
自定义配置将以和默认配置合并, 在 nginx 重启或者通过接口 /waf/config/reload
重载配置后失效
配置合并的规则
对于模块的
rules
配置, 只要设置了就会完全替换默认配置, 否则保留默认配置对于
matchers
,responses
等采用 修改原有 + 新增 的方式进行合并, 会保留已经存在的默认配置
curl --request POST 'http://127.0.0.1/waf/config' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==' \
--data-raw '{
"modules": {
"counter": {
"enable": true,
"rules": [
{
"matcher": "any",
"by": "ip",
"time": 86400,
"enable": true
},
{
"matcher": "any",
"by": "ip,uri",
"time": 86400,
"enable": true
}
]
}
}
}'
4.2 自定义配置 list
自定义配置将以覆盖模式和当前 list
*合并*
curl --location --request POST 'http://127.0.0.1/waf/list' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==' \
--data-raw '{
"127.0.0.1": 6000, // 将IP:127.0.0.1放入名单, ttl为6000秒
"30000000": 86400,
"832489A9-2442-4E87-BD6B-24D85B05FB25": 3600
}'
5. 自定义配置(持续生效, 通过 Redis)
默认读取环境变量 REDIS_HOST
, REDIS_PORT
, REDIS_DB
来获取 redis 配置, 否则从 /data/.env
读取
5.1 自定义配置 config
配置合并的规则
对于模块的
rules
配置, 只要设置了就会完全替换默认配置, 否则保留默认配置对于
matcher
,response
等采用 修改原有 + 新增 的方式进行合并, 会保留已经存在的默认配置
config 存放于 redis 中以
waf:config:
为开头的hset
中目前支持几个配置项,
waf:config:matchers
**waf:config:responses
**waf:config:moduules:manager:auth
**waf:config:moduules:filter:rules
**waf:config:moduules:limiter:rules
**waf:config:moduules:counter:rules
**waf:config:moduules:sampler:rules
**waf:config:moduules:filter
**(仅支持对enable
进行设置)waf:config:moduules:limiter
**(仅支持对enable
进行设置)waf:config:moduules:counter
**(仅支持对enable
进行设置)waf:config:moduules:sampler
**(仅支持对enable
进行设置)
如在
redis
中执行命令hset waf:config:moduules:counter enable false
**在 redis 配置后需执行
/waf/config/reload
** 将配置与默认配置进行合并, 方可生效
5.2 自定义配置 list
自定义的 list 放在 redis 中以
waf:list
** 为 key 的zset
中如在
redis
中执行命令zadd waf:list 1666267510 127.0.0.1
**在 redis 配置后需执行
/waf/list/reload
** 将配置与当前共享内存名单合并后生效
6. 应用场景示范
6.1 维护 IP/uid/device 名单
示例一: 限制访问(默认配置已经在 filter
模块中开启了对 list
名单的支持, 默认为黑名单)
// 限制设备号`X-Device-ID` = `f14268d542f919d5` 访问, 在到达Unix time 1666267510 之前
zadd waf:list 1666267510 f14268d542f919d5
// 限制IP `13.251.156.174` 的访问, 在到达Unix time 1666267510 之前
zadd waf:list 1666267510 13.251.156.174
// 重载配置
curl --request POST 'http://127.0.0.1/waf/list/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
示例二: 允许访问 (修改默认配置, 将 list
用作白名单)
在 redis 中执行
hset waf:config:moduules:filter:rules 1 '{"matcher":"any","action":"accept","enable":true,"by":"ip:in_list"}'
hset waf:config:moduules:filter:rules 0 '{"matcher":"any","action":"block","enable":true,"by":"ip:not_in_list"}'
zadd waf:list 1666267510 13.251.156.174
重载配置及名单后生效
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
curl --request POST 'http://127.0.0.1/waf/list/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.2 配置 matcher
// 匹配头部参数 X-App-ID = 4 的请求
hset waf:config:matchers app_id '{"Header":{"operator":"#","name":"x-app-id","value":[4]}}'
// 匹配 UserAgent 包含 "postman" 的请求
hset waf:config:matchers attack_agent '{"UserAgent":{"value":"(postman)","operator":"≈"}}'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.3 配置 response
// Redis 命令
hset waf:config:responses 503 '{"status":503,"mime_type":"application/json","body":"{\"code\":\"503\", \"message\":\"Custom Message\"}"}'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.4 moduules:filter:rules
// Redis 命令
hset waf:config:moduules:filter:rules 0 '{"matcher":"any","action":"block","enable":true,"by":"ip:not_in_list"}'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.5 moduules:limiter:rules
// Redis 命令
hset waf:config:moduules:limiter:rules 0 '{"code":403,"count":60,"time":60,"matcher":"any","by":"ip","enable":true}'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.6 moduules:counter:rules
// Redis 命令
hset waf:config:moduules:counter:rules 0 '{"matcher":"any","by":"ip,uri","time":60,"enable":true}'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
6.7 修改 moduules:manager
// Redis 命令
hset waf:config:moduules:manager:auth '{"user": "test", "pass": "123" }'
// 重载配置
curl --request POST 'http://127.0.0.1/waf/config/reload' \
--header 'Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ=='
7. 参考项目
8. OpenResty 一些知识
8.1 模块里的变量
处于模块级别的变量在每个 worker 间是相互独立的,且在 worker 的生命周期中是只读的, 只在第一次导入模块时初始化.
模块里函数的局部变量, 则在调用时初始化
8.2 ngx.var.*
使用代价较高
需先预定义才可使用(可在 server 或 location 中定义)
类型只能是字符串
内部重定向会破坏原始请求的
ngx.var.*
变量 (如error_page
,try_files
,index
等)
8.3 ngx.ctx.*
内部重定向会破坏原始请求的
ngx.ctx.*
变量 (如error_page
,try_files
,index
等)
8.4 ngx.shared.DICT.*
可在不同 worker 间共享数据
8.5 resty.lrucache
不同 worker 间数据相互隔离
同一 worker 不同请求共享数据
https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker
8.6 table 与 metatable
https://www.cnblogs.com/liekkas01/p/12728712.html
9 如何开发
// 环境建立
git clone https://github.com/Kiuber/lua-resty-waf.git
cd lua-resty-waf
touch .opmrc
docker-compose up -d
// 编码
...
// 打包
docker exec -it resty opm build
docker exec -it resty opm upload
10. 一些相关链接
OpenResty LuaJIT2 https://github.com/openresty/luajit2#tablenkeys
Lua 手册 Lua 5.4
Resty 模块 OpenResty
Resty 模块 Lua-Resty-JWT
POD 错误
注意!以上文档存在一些编码错误,解释如下:
- 大约在第 831 行
-
未终止的 B<...> 序列
- 大约在第 835 行
-
未终止的 B<...> 序列
- 大约在第 839 行
-
未终止的 B<...> 序列
- 大约在第 843 行
-
未终止的 B<...> 序列
- 大约在第 847 行
-
未终止的 B<...> 序列
- 大约在第 851 行
-
未终止的 B<...> 序列
- 大约在第 855 行
-
未终止的 B<...> 序列
- 大约在第 859 行
-
未终止的 B<...> 序列
- 大约在第 863 行
-
未终止的 B<...> 序列
- 大约在第 867 行
-
未终止的 B<...> 序列
- 大约在第 871 行
-
未终止的 B<...> 序列
- 大约在第 878 行
-
未终止的 B<...> 序列
- 大约在第 882 行
-
未终止的 B<...> 序列
- 大约在第 897 行
-
未终止的 B<...> 序列
- 大约在第 901 行
-
未终止的 B<...> 序列
- 大约在第 905 行
-
未终止的 B<...> 序列
作者
kiuber
许可证
2bsd
依赖
xiangnanscu/lua-resty-inspect >= 1.0, SkyLothar/lua-resty-jwt >= 0.1.11
版本
-
Kiuber/lua-resty-waf 1.2.5基于 OpenResty 的 Lua 编写的简单 WAF 2023-05-28 15:07:33
-
Kiuber/lua-resty-waf 1.2.4基于 OpenResty 的 Lua 编写的简单 WAF 2023-05-28 13:53:28
-
Kiuber/lua-resty-waf 1.2.3基于 OpenResty 的 Lua 编写的简单 WAF 2023-05-28 13:44:59
-
Kiuber/lua-resty-waf 1.2.2基于 OpenResty 的 Lua 编写的简单 WAF 2023-05-28 07:55:20