lua-resty-waf

Simple WAF based on OpenResty written by Lua

$ opm get Kiuber/lua-resty-waf

项目说明

0. 安装使用

  • 本项目 fork of 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重载配置后失效

配置合并的规则:

  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/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. 一些相关链接

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 831:

Unterminated B<...> sequence

Around line 835:

Unterminated B<...> sequence

Around line 839:

Unterminated B<...> sequence

Around line 843:

Unterminated B<...> sequence

Around line 847:

Unterminated B<...> sequence

Around line 851:

Unterminated B<...> sequence

Around line 855:

Unterminated B<...> sequence

Around line 859:

Unterminated B<...> sequence

Around line 863:

Unterminated B<...> sequence

Around line 867:

Unterminated B<...> sequence

Around line 871:

Unterminated B<...> sequence

Around line 878:

Unterminated B<...> sequence

Around line 882:

Unterminated B<...> sequence

Around line 897:

Unterminated B<...> sequence

Around line 901:

Unterminated B<...> sequence

Around line 905:

Unterminated B<...> sequence

Authors

kiuber

License

2bsd

Dependencies

Versions