- 本项目基于 OpenResty,所以需要先安装好 OpenResty, Linux各发行版安装详见OpenResty® Linux 包
- 通过 OpenResty 的包管理器
opm
安装本项目opm get codiy1992/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",
})
}
}
当可用内存不足时, 将自动覆盖最久未被使用的未过期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;
存放采样器的采样信息
init_worker_by_lua
阶段, 读入默认配置, 并从 redis 获取最新配置信息, 合并两者放入共享内存access_by_lua
阶段, 从共享内存读取配置, 顺序执行对应模块
配置由三大部分组成如下
matchers
一些匹配规则, 可在各模块间共用, 用于匹配特定请求responses
自定义响应格式, 可在各模块间共用, 用于waf模块内的http响应modules
模块配置, 包含manager
,filter
,limiter
,counter
,sampler
五大模块
在模块内根据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": "#"
}
}
}
用于waf
模块拒绝请求时候响应给客户端
默认配置如下, 可自行增加或修改配置
{
"403": { // 对于各模块规则中的`code`, 不需要与HTTP的`status code`对应
"status": 403, // HTTP的`status code`
"body": "{\"code\":\"403\", \"message\":\"403 Forbidden\"}",
"mime_type": "application/json"
}
}
用于 waf 的管理, 提供一系列以 /waf
开头的路由, 需要通过 Basic Authorizaton 认证
默认账号密码 waf:TTpsXHtI5mwq
或者指定头信息 Authorization: Basic d2FmOlRUcHNYSHRJNW13cQ==
可使用项目根目录下的postman.json
导入postman进行使用
路由 | METHOD | 用途 |
---|---|---|
/waf/status |
GET | 获取状态信息 |
/waf/config |
GET | 获取当前配置 |
/waf/config |
POST | 临时变更配置 |
/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 | 查询采集器里的采样数据 |
用于过滤请求,流程如下
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
}
]
}
用于请求频率限制,对于匹配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)
}'
统计请求次数,根据 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)
}'
采样器, 模块支持两个内置的额外 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
}'
{
"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"
}
]
}
}
}
自定义配置将以和默认配置合并, 在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
}
]
}
}
}'
自定义配置将以覆盖模式和当前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
}'
默认读取环境变量REDIS_HOST
,REDIS_PORT
,REDIS_DB
来获取redis配置, 否则从 /data/.env
读取
配置合并的规则:
- 对于模块的
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
将配置与默认配置进行合并,方可生效
- 自定义的list放在 redis 中以
waf:list
为key的zset
中 - 如在
redis
中执行命令zadd waf:list 1666267510 127.0.0.1
- 在 redis 配置后需执行
/waf/list/reload
将配置与当前共享内存名单合并后生效
示例一: 限制访问(默认配置已经在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=='
// 匹配头部参数 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=='
// 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=='
// 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=='
// 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=='
// 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=='
// 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=='
- 处于模块级别的变量在每个 worker 间是相互独立的,且在 worker 的生命周期中是只读的, 只在第一次导入模块时初始化.
- 模块里函数的局部变量,则在调用时初始化
- lua-nginx-module#ngxvarvariable
- 使用代价较高
- 续先预定义才可使用(可在server 或 location 中定义)
- 类型只能是字符串
- 内部重定向会破坏原始请求的
ngx.var.*
变量 (如error_page
,try_files
,index
等)
- lua-nginx-module#ngxctx
- 内部重定向会破坏原始请求的
ngx.ctx.*
变量 (如error_page
,try_files
,index
等)
- 可在不同 worker 间共享数据
- lua-nginx-module#ngxshareddict
- data-sharing-within-an-nginx-worker
- lua-resty-lrucache
- 不同 worker 间数据相互隔离
- 同一 worker 不同请求共享数据
https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker
https://www.cnblogs.com/liekkas01/p/12728712.html
// 环境建立
git clone https://github.com/codiy1992/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
- OpenResty LuaJIT2 https://github.com/openresty/luajit2#tablenkeys
- Lua 手册Lua 5.4
- Resty 模块OpenResty
- Resty 模块Lua-Resty-JWT