Skip to content

codiy1992/lua-resty-waf

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

项目说明

0. 安装使用

  • 本项目基于 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", }) } }

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 临时变更配置
/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重载配置后失效

配置合并的规则:

  1. 对于模块的rules配置, 只要设置了就会完全替换默认配置, 否则保留默认配置
  2. 对于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

配置合并的规则:

  1. 对于模块的rules配置, 只要设置了就会完全替换默认配置, 否则保留默认配置
  2. 对于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.*

  • lua-nginx-module#ngxvarvariable
  • 使用代价较高
  • 续先预定义才可使用(可在server 或 location 中定义)
  • 类型只能是字符串
  • 内部重定向会破坏原始请求的 ngx.var.* 变量 (如 error_page, try_files, index 等)

8.3 ngx.ctx.*

  • lua-nginx-module#ngxctx
  • 内部重定向会破坏原始请求的 ngx.ctx.* 变量 (如 error_page, try_files, index 等)

8.4 ngx.shared.DICT.*

8.5 resty.lrucache

  • lua-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/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

10. 一些相关链接

About

Simple WAF based on OpenResty written by Lua

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published