├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── conf └── waf.conf ├── lua_lib └── el7luajit2 │ ├── ahocorasick.so │ ├── cjson.so │ ├── libac.so │ └── libinjection.so ├── lua_scripts ├── config.lua ├── module │ ├── actions.lua │ ├── cc.lua │ ├── cookie.lua │ ├── filter.lua │ ├── gzipd.lua │ ├── ip_cidr.lua │ ├── kafka.lua │ ├── libinjection.lua │ ├── load_ac.lua │ ├── log.lua │ ├── operators.lua │ ├── request.lua │ ├── rule.lua │ ├── transform.lua │ ├── utils.lua │ └── white_list.lua ├── on_access.lua ├── on_body_filter.lua ├── on_header_filter.lua ├── on_init.lua ├── on_log.lua └── on_worker_init.lua └── sample_config.json /.gitignore: -------------------------------------------------------------------------------- 1 | # mac 2 | .DS_Store 3 | 4 | # emacs 5 | \#*\# 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lua-aho-corasick"] 2 | path = lua-aho-corasick 3 | url = https://github.com/cloudflare/lua-aho-corasick 4 | [submodule "libinjection"] 5 | path = libinjection 6 | url = https://github.com/client9/libinjection 7 | [submodule "lua-cjson"] 8 | path = lua-cjson 9 | url = https://github.com/mpx/lua-cjson.git 10 | [submodule "zlib"] 11 | path = zlib 12 | url = https://github.com/luapower/zlib 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) [2019], [ZhongAnTech] 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | maive_waf 2 | === 3 | 众安开源waf引擎 4 | 5 | 简介 6 | === 7 | 基于OpenResty®实现的高性能应用防火墙(WAF),需搭配众安开源[waf控制台](https://github.com/ZhongAnTech/maiev-waf-web.git)项目一起使用。 8 | 9 | 特性 10 | === 11 | * 每个host可使用独立规则集,自定义防护策略,互不干扰 12 | * 支持对request进行过滤,全http实体字段过滤,包括上传文件 13 | * 支持对响应头过滤 14 | * 支持响应体过滤 15 | * 解决了传统Resty WAF参数个数超100个的绕过攻击 16 | * 支持白名单,黑名单 17 | * IPv6支持 18 | 19 | 状态 20 | === 21 | 截止2019年底已在众安生产环境部署使用3年。 22 | 23 | 版本 24 | === 25 | v4.0 26 | 27 | 安装 28 | === 29 | * 1. 安装OpenResty (>= 1.11.2.4) ,参照官方安装文档安装即可。由于规则可能大量涉及到正则匹配的操作,启用pcre-jit总能让性能更好,前提是操作系统支持。 30 | > ./configure --with-pcre-jit 31 | * 2. 克隆仓库 32 | > git clone https://github.com/ZhongAnTech/maiev-waf.git 33 | * 3. 将包复制到openresty安装目录 34 | > cp -rf conf lua_scripts /usr/local/openresty/nginx/ 35 | > 请确保nginx用户对该目录/usr/local/openresty/nginx/及子目录有读取权限。 36 | * 4. 手动编译依赖的动态链接库(libinjection,cjson,ahocorasick, zlib) 37 | 进入到对应子仓库目录下,执行Makefile编译对应操作系统平台的动态链接库文件。将编译成功后的so文件拷贝到/usr/local/openresty/lualib下。为方便大家使用,本项目已经预编译好了CentOS7下的所有动态库,放在lua_lib/el7luajit2下。将其拷贝/usr/local/openresty/lualib下即可。 38 | ``` 39 | lua_lib/el7luajit2 40 | ├── ahocorasick.so 41 | ├── cjson.so 42 | ├── libac.so 43 | └── libinjection.so 44 | ``` 45 | 46 | 配置 47 | === 48 | * lua_scripts/config.lua文件是waf初始化配置文件,包括规则配置文件路径,日志配置。 49 | 50 | * 日志配置支持kafka和syslog两种方式,不建议本地文件的方式,虽然也支持。如果采用syslog方式需要使用到[lua-resty-logger-socket](https://github.com/cloudflare/lua-resty-logger-socket.git)。如果采用kafka方式需要使用到[lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka.git)。 51 | 52 | * 规则配置,waf启动时会自动加载config.lua里指定的config文件,json格式,参考sample_config.json。除非对仓库代码很理解,否则不建议手动编辑规则json文件。请使用众安开源的waf控制台项目进行规则创建,配合salt api自动下发config.json文件到waf目录。 53 | > cp sample_config.json /usr/local/openresty/nginx/config.json 54 | 55 | * OpenResty的nginx.conf配置文件http块增加一行引入waf.conf,参考如下: 56 | 57 | ```json 58 | http { 59 | ... 60 | include /usr/local/openresty/nginx/conf/waf.conf; 61 | ... 62 | } 63 | ``` 64 | 65 | * 如果任意配置文件(config.lua, config.json, waf.conf)有更新,均需要nginx -s reload重新加载。 66 | > /usr/local/openresty/bin/openresty -s reload 67 | 68 | 性能测试 69 | === 70 | 以下是在2C1G的阿里云服务器nginx开启8个worker后用Jmeter测试10并发10分钟的结果 71 | 72 | | 模式 | 吞吐量(KB/s) | tps | 95%响应时间ms | 73 | |-----------|-------|-----|--------------| 74 | | 关闭 | 1361 | 7262 | 2 | 75 | | 0条规则 | 777 | 4147 | 4 | 76 | | 10条规则 | 57 | 219 | 103 | 77 | 78 | ### 结论 79 | 可以看出基于nginx的OpenResty性能非常优秀的,waf影响性能的地方在于对http流量的过滤,所以在测试的时候会参考waf关闭和开启时对请求的影响,在10个规则过滤的前提,测试下来的结果是对整个请求影响100ms左右。当然测试结果会随着规则的变化而发生变化,比如规则里开启了对响应体的过滤,则对响应体较大的请求影响比较大。 80 | 81 | LICENSE 82 | === 83 | BSD 84 | -------------------------------------------------------------------------------- /conf/waf.conf: -------------------------------------------------------------------------------- 1 | lua_package_path "/usr/local/openresty/lualib/?.lua;;/usr/local/openresty/nginx/lua_scripts/?.lua;;/usr/local/openresty/nginx/lua_scripts/module/?.lua;;"; 2 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 3 | lua_shared_dict cfg 1m; 4 | lua_shared_dict cc 1m; 5 | init_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_init.lua; 6 | init_worker_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_worker_init.lua; 7 | access_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_access.lua; 8 | header_filter_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_header_filter.lua; 9 | body_filter_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_body_filter.lua; 10 | log_by_lua_file /usr/local/openresty/nginx/lua_scripts/on_log.lua; 11 | -------------------------------------------------------------------------------- /lua_lib/el7luajit2/ahocorasick.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongAnTech/maiev-waf/60b55e17006faf29c678e7a7cddc26c8b5f4ddf3/lua_lib/el7luajit2/ahocorasick.so -------------------------------------------------------------------------------- /lua_lib/el7luajit2/cjson.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongAnTech/maiev-waf/60b55e17006faf29c678e7a7cddc26c8b5f4ddf3/lua_lib/el7luajit2/cjson.so -------------------------------------------------------------------------------- /lua_lib/el7luajit2/libac.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongAnTech/maiev-waf/60b55e17006faf29c678e7a7cddc26c8b5f4ddf3/lua_lib/el7luajit2/libac.so -------------------------------------------------------------------------------- /lua_lib/el7luajit2/libinjection.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongAnTech/maiev-waf/60b55e17006faf29c678e7a7cddc26c8b5f4ddf3/lua_lib/el7luajit2/libinjection.so -------------------------------------------------------------------------------- /lua_scripts/config.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local cjson = require 'cjson.safe' or require 'cjson' 3 | local utils = require('utils') 4 | local cfg_cache = ngx.shared.cfg 5 | 6 | local _M = {} 7 | 8 | if _M then 9 | -- rule config.json path 10 | _M.cfg_backup_file = '/usr/local/oprensty/nginx/config.json' 11 | -- log type: syslog|kafka 12 | _M.log_type = 'syslog' 13 | _M.syslog_host = 'your_rsyslog_server_ip' 14 | _M.syslog_port = 514 15 | _M.syslog_sock_type = "udp" 16 | _M.kafka_broker_list = { 17 | {host = "your_kafka_broker_list", port=9092} 18 | } 19 | _M.kafka_topics = { 20 | ngx_topic = 'ngx-access-topic', 21 | waf_topic = 'waf-attack-topic' 22 | } 23 | end 24 | 25 | _M.configs = nil 26 | 27 | local function is_valid_cfg(configs) 28 | local result = configs.code == 200 and configs.rules ~= nil and configs.version ~= nil and configs.datas ~= nil 29 | print('waf cfg check ', tostring(result)) 30 | return result 31 | end 32 | 33 | local function get_cfg_from_backup_file() 34 | local configs = utils.read_small_file(_M.cfg_backup_file) 35 | if configs then 36 | configs = cjson.decode(configs) 37 | if configs and is_valid_cfg(configs) then 38 | local hosts_config = {} 39 | for _, d in ipairs(configs.datas) do 40 | -- add rule detail in filters 41 | for _, f in ipairs(d.data.filters) do 42 | if configs.rules[f.rule_id] ~= nil then 43 | f['rule'] = configs.rules[f.rule_id] 44 | else 45 | ngx.log(ngx.ERR, 'invalid rule id in cfg:' .. f.rule_id) 46 | print('invalid rule_id in filters:' .. cjson.encode(f)) 47 | return 48 | end 49 | end 50 | -- add rule detail in body filters 51 | for _, f in ipairs(d.data.body_filters) do 52 | if configs.rules[f.rule_id] ~= nil then 53 | f['rule'] = configs.rules[f.rule_id] 54 | else 55 | ngx.log(ngx.ERR, 'invalid rule id in cfg:' .. f.rule_id) 56 | print('invalid rule_id in body filters:' .. cjson.encode(f)) 57 | return 58 | end 59 | end 60 | -- add rule detail in header filters 61 | for _, f in ipairs(d.data.header_filters) do 62 | if configs.rules[f.rule_id] ~= nil then 63 | f['rule'] = configs.rules[f.rule_id] 64 | else 65 | ngx.log(ngx.ERR, 'invalid rule id in cfg:' .. f.rule_id) 66 | print('invalid rule_id in header filters:' .. cjson.encode(f)) 67 | return 68 | end 69 | end 70 | hosts_config[d.site_name] = d.data 71 | end 72 | return hosts_config 73 | end 74 | end 75 | end 76 | 77 | local function init() 78 | --cosocket disabled in init_by_lua, init_worker_by_lua 79 | --local configs, hash = get_cfg_from_console() 80 | --[[ nginx HUP指令并不会清空shared.dict 81 | if configs ~= nil then 82 | cfg:set('waf_configs', nil) 83 | end 84 | --]] 85 | local configs = get_cfg_from_backup_file() 86 | if configs then 87 | print('waf cfg is valid', cjson.encode(configs)) 88 | _M.configs = configs 89 | else 90 | ngx.log(ngx.ERR, 'waf cfg is invalid') 91 | end 92 | end 93 | 94 | _M.init = init 95 | _M.WAF_ON = 1 96 | _M.WAF_LOG = 0 97 | _M.WAF_OFF = -1 98 | 99 | return _M 100 | -------------------------------------------------------------------------------- /lua_scripts/module/actions.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local _M = {} 3 | 4 | _M.disruptive_lookup = { 5 | APPEND = function(content) 6 | local response_body = ngx.ctx.response_body 7 | if response_body then 8 | ngx.ctx.response_body = response_body .. content 9 | end 10 | end, 11 | EMPTY = function(content) 12 | ngx.ctx.response_body = nil 13 | return 14 | [[ local response_body = ngx.ctx.response_body 15 | if response_body then 16 | --error('detect reponse error') 17 | return 18 | end 19 | ]] 20 | end, 21 | GSUB = function(pattern, new, pcre_flags) 22 | local response_body = ngx.ctx.response_body 23 | if type(response_body) == 'string' then 24 | print('reponse to replace: ', response_body, ', pattern: ', pattern, ', new: ', new) 25 | ngx.ctx.response_body = ngx.re.gsub(response_body, pattern, new, pcre_flags) 26 | end 27 | end, 28 | ACCEPT = function() 29 | --ngx.exit(ngx.OK) 30 | return 31 | end, 32 | DENY = function() 33 | ngx.exit(ngx.HTTP_FORBIDDEN) 34 | end, 35 | DROP = function() 36 | --ngx.exit(ngx.HTTP_CLOSE) 37 | ngx.exit(444) 38 | end, 39 | MAX_BODY = function() 40 | ngx.exit(413) 41 | end, 42 | REDIRECT = function(redirect_url, status) 43 | ngx.redirect(redirect_url, status or ngx.HTTP_MOVED_TEMPORARILY) 44 | end 45 | --[[-该动作可能不生效,需测试,正常rewrite执行是在rewrite_by_lua阶段执行 46 | REWRITE = function(rewirte_uri) 47 | --local url = ngx.re.sub(ngx.var.url, src, desc) 48 | ngx.req.set_uri(rewrite_uri, true) 49 | end 50 | --]] 51 | } 52 | 53 | return _M 54 | -------------------------------------------------------------------------------- /lua_scripts/module/cc.lua: -------------------------------------------------------------------------------- 1 | local actions = require('actions') 2 | local log = require('log') 3 | local cfg = require('config') 4 | 5 | local cc_cache = ngx.shared.cc 6 | 7 | local CC_ON = 1 8 | local CC_LOG = 0 9 | local CC_OFF = -1 10 | 11 | local _M = {} 12 | 13 | local function filter(host, host_cfg) 14 | local client_ip = ngx.var.remote_addr 15 | if client_ip then 16 | local key = host..client_ip..ngx.var.uri 17 | --print('-----cc key', key) 18 | if type(host_cfg.cc_max) == 'number' and 19 | type(host_cfg.cc_period) == 'number' then 20 | 21 | count = cc_cache:get(key) 22 | if count then 23 | if count >= host_cfg.cc_max then 24 | 25 | local log_only = true 26 | if host_cfg.waf_mode == cfg.WAF_ON and host_cfg.cc_mode == CC_ON then 27 | log_only = false 28 | end 29 | log.log_attack(-1, 'cc attack', 'cc', 'unknow', 'DROP', log_only) 30 | if not log_only then 31 | actions.disruptive_lookup.DROP() 32 | end 33 | return true 34 | else 35 | cc_cache:incr(key, 1) 36 | end 37 | else 38 | cc_cache:set(key, 1, host_cfg.cc_period) 39 | end 40 | end 41 | end 42 | return false 43 | end 44 | 45 | _M.filter = filter 46 | 47 | return _M 48 | -------------------------------------------------------------------------------- /lua_scripts/module/cookie.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2013 Jiale Zhi (calio), Cloudflare Inc. 2 | -- See RFC6265 http://tools.ietf.org/search/rfc6265 3 | -- require "luacov" 4 | 5 | local type = type 6 | local byte = string.byte 7 | local sub = string.sub 8 | local format = string.format 9 | local log = ngx.log 10 | local ERR = ngx.ERR 11 | local ngx_header = ngx.header 12 | 13 | local EQUAL = byte("=") 14 | local SEMICOLON = byte(";") 15 | local SPACE = byte(" ") 16 | local HTAB = byte("\t") 17 | 18 | -- table.new(narr, nrec) 19 | local ok, new_tab = pcall(require, "table.new") 20 | if not ok then 21 | new_tab = function () return {} end 22 | end 23 | 24 | local ok, clear_tab = pcall(require, "table.clear") 25 | if not ok then 26 | clear_tab = function(tab) for k, _ in pairs(tab) do tab[k] = nil end end 27 | end 28 | 29 | local _M = new_tab(0, 2) 30 | 31 | _M._VERSION = '0.01' 32 | 33 | 34 | local function get_cookie_table(text_cookie) 35 | if type(text_cookie) ~= "string" then 36 | log(ERR, format("expect text_cookie to be \"string\" but found %s", 37 | type(text_cookie))) 38 | return {} 39 | end 40 | 41 | local EXPECT_KEY = 1 42 | local EXPECT_VALUE = 2 43 | local EXPECT_SP = 3 44 | 45 | local n = 0 46 | local len = #text_cookie 47 | 48 | for i=1, len do 49 | if byte(text_cookie, i) == SEMICOLON then 50 | n = n + 1 51 | end 52 | end 53 | 54 | local cookie_table = new_tab(0, n + 1) 55 | 56 | local state = EXPECT_SP 57 | local i = 1 58 | local j = 1 59 | local key, value 60 | 61 | while j <= len do 62 | if state == EXPECT_KEY then 63 | if byte(text_cookie, j) == EQUAL then 64 | key = sub(text_cookie, i, j - 1) 65 | state = EXPECT_VALUE 66 | i = j + 1 67 | end 68 | elseif state == EXPECT_VALUE then 69 | if byte(text_cookie, j) == SEMICOLON 70 | or byte(text_cookie, j) == SPACE 71 | or byte(text_cookie, j) == HTAB 72 | then 73 | value = sub(text_cookie, i, j - 1) 74 | cookie_table[key] = value 75 | 76 | key, value = nil, nil 77 | state = EXPECT_SP 78 | i = j + 1 79 | end 80 | elseif state == EXPECT_SP then 81 | if byte(text_cookie, j) ~= SPACE 82 | and byte(text_cookie, j) ~= HTAB 83 | then 84 | state = EXPECT_KEY 85 | i = j 86 | j = j - 1 87 | end 88 | end 89 | j = j + 1 90 | end 91 | 92 | if key ~= nil and value == nil then 93 | cookie_table[key] = sub(text_cookie, i) 94 | end 95 | 96 | return cookie_table 97 | end 98 | 99 | function _M.new(self) 100 | local _cookie = ngx.var.http_cookie 101 | --if not _cookie then 102 | --return nil, "no cookie found in current request" 103 | --end 104 | return setmetatable({ _cookie = _cookie, set_cookie_table = new_tab(4, 0) }, 105 | { __index = self }) 106 | end 107 | 108 | function _M.get(self, key) 109 | if not self._cookie then 110 | return nil, "no cookie found in the current request" 111 | end 112 | if self.cookie_table == nil then 113 | self.cookie_table = get_cookie_table(self._cookie) 114 | end 115 | 116 | return self.cookie_table[key] 117 | end 118 | 119 | function _M.get_all(self) 120 | if not self._cookie then 121 | return nil, "no cookie found in the current request" 122 | end 123 | 124 | if self.cookie_table == nil then 125 | self.cookie_table = get_cookie_table(self._cookie) 126 | end 127 | 128 | return self.cookie_table 129 | end 130 | 131 | local function bake(cookie) 132 | if not cookie.key or not cookie.value then 133 | return nil, 'missing cookie field "key" or "value"' 134 | end 135 | 136 | if cookie["max-age"] then 137 | cookie.max_age = cookie["max-age"] 138 | end 139 | local str = cookie.key .. "=" .. cookie.value 140 | .. (cookie.expires and "; Expires=" .. cookie.expires or "") 141 | .. (cookie.max_age and "; Max-Age=" .. cookie.max_age or "") 142 | .. (cookie.domain and "; Domain=" .. cookie.domain or "") 143 | .. (cookie.path and "; Path=" .. cookie.path or "") 144 | .. (cookie.secure and "; Secure" or "") 145 | .. (cookie.httponly and "; HttpOnly" or "") 146 | .. (cookie.extension and "; " .. cookie.extension or "") 147 | return str 148 | end 149 | 150 | function _M.set(self, cookie) 151 | local cookie_str, err = bake(cookie) 152 | if not cookie_str then 153 | return nil, err 154 | end 155 | 156 | local set_cookie = ngx_header['Set-Cookie'] 157 | local set_cookie_type = type(set_cookie) 158 | local t = self.set_cookie_table 159 | clear_tab(t) 160 | 161 | if set_cookie_type == "string" then 162 | -- only one cookie has been setted 163 | if set_cookie ~= cookie_str then 164 | t[1] = set_cookie 165 | t[2] = cookie_str 166 | ngx_header['Set-Cookie'] = t 167 | end 168 | elseif set_cookie_type == "table" then 169 | -- more than one cookies has been setted 170 | local size = #set_cookie 171 | 172 | -- we can not set cookie like ngx.header['Set-Cookie'][3] = val 173 | -- so create a new table, copy all the values, and then set it back 174 | for i=1, size do 175 | t[i] = ngx_header['Set-Cookie'][i] 176 | if t[i] == cookie_str then 177 | -- new cookie is duplicated 178 | return true 179 | end 180 | end 181 | t[size + 1] = cookie_str 182 | ngx_header['Set-Cookie'] = t 183 | else 184 | -- no cookie has been setted 185 | ngx_header['Set-Cookie'] = cookie_str 186 | end 187 | return true 188 | end 189 | 190 | return _M 191 | -------------------------------------------------------------------------------- /lua_scripts/module/filter.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- 基于自定义规则过滤 3 | local cfg = require('config') 4 | --[[ 5 | local configs = cfg.configs 6 | local rules = configs.rules 7 | local filters = configs.filters 8 | --]] 9 | local log = require('log') 10 | --local request_tester = require('request_tester') 11 | local rule_engine = require('rule') 12 | local actions = require('actions') 13 | 14 | local _M = {} 15 | 16 | --常量化魔法变量 17 | local F_ON = 1 18 | local F_OFF = -1 19 | local F_LOG = 0 20 | local ACCEPT = 'ACCEPT' 21 | local DENY = 'DENY' 22 | local DROP = 'DROP' 23 | local REDIRECT = 'REDIRECT' 24 | local REWRITE = 'REWRITE' 25 | local RESPONSE_REPLACE = 'GSUB' 26 | 27 | local function do_action(filter) 28 | local action = filter.action 29 | if actions.disruptive_lookup[action] then 30 | if action == REDIRECT and filter.action_vars and filter.action_vars.redirect_url then 31 | local redirect_url = filter.action_vars.redirect_url 32 | local rule_arg = 'aegis_rule_id=' .. filter.rule_id 33 | if string.find(redirect_url, '?') then 34 | redirect_url = redirect_url .. '&' .. rule_arg 35 | else 36 | redirect_url = redirect_url .. '?' .. rule_arg 37 | end 38 | print('-----redirect_url'..redirect_url) 39 | actions.disruptive_lookup[action](redirect_url) 40 | elseif action == RESPONSE_REPLACE then 41 | local pattern = filter.action_vars.pattern 42 | local new = filter.action_vars.new 43 | local pcre_flags = filter.action_vars.pcre_flags or 'ioj' 44 | print('reponse replace', ', pattern: ', pattern, ', new: ', new) 45 | actions.disruptive_lookup[action](pattern, new, pcre_flags) 46 | else 47 | actions.disruptive_lookup[action]() 48 | end 49 | else 50 | ngx.log(ngx.ERR, '---invalid action:'..tostring(action)) 51 | end 52 | end 53 | 54 | local function _get_body_filters(host_cfg) 55 | local filters = {} 56 | local op = 'GET' 57 | if host_cfg and host_cfg.request_body_access == F_ON then 58 | local nofile_limit = host_cfg.request_body_nofile_limit 59 | print('request nofile limit: ', nofile_limit) 60 | filters[1] = { 61 | mode=1, action="MAX_BODY", rule_id="-2", 62 | rule={ 63 | risk_level=3, 64 | attack_tag="Request Entity Nofile Too Big", 65 | desc="Request Entity Nofile Too Big", 66 | match={ 67 | { 68 | transforms={}, 69 | operator=op, 70 | value=nofile_limit, 71 | items={"BODY_NOFILE_SIZE"} 72 | } 73 | } 74 | } 75 | } 76 | if host_cfg.upload_file_access == F_ON then 77 | local body_limit = host_cfg.request_body_limit 78 | print('request full body limit: ', body_limit) 79 | filters[2] = { 80 | mode=1, action="MAX_BODY", rule_id="-3", 81 | rule={risk_level=3, 82 | attack_tag="Request Entity Full Too Big", 83 | desc="Request Entity Full Too Big", 84 | match={ 85 | { 86 | transforms={}, 87 | operator=op, 88 | value=body_limit, 89 | items={"UPLOAD_FILE_SIZE"} 90 | } 91 | }} 92 | } 93 | end 94 | end 95 | return filters 96 | end 97 | 98 | local function filter(host_cfg, filter_phase) 99 | if filter_phase == 'body' then 100 | filters = host_cfg.body_filters 101 | print('=======filter body', ngx.ctx.response_body) 102 | elseif filter_phase == 'header' then 103 | filters = host_cfg.header_filters 104 | else 105 | filters = host_cfg.filters 106 | local body_filters = _get_body_filters(host_cfg) 107 | for idx, f in ipairs(body_filters) do 108 | table.insert(filters, idx, f) 109 | end 110 | end 111 | for _, filter in pairs(filters) do 112 | --filter设置3个状态是为了测试新加过滤器,生产环境调试规则会方便 113 | if filter and filter.mode >= F_LOG then 114 | local rule = filter.rule 115 | print('start match rule, id: ', filter.rule_id) 116 | --把rule id 放入ctx,后续创建ac_dicts需要 117 | ngx.ctx.rule_id = filter.rule_id 118 | local matcher = rule.match 119 | --ngx.log(ngx.ERR, 'rule'..cjson.encode(rule)..'match'..cjson.encode(matcher)) 120 | if rule_engine.match(matcher) == true then 121 | --命中规则 122 | local log_only = true 123 | if host_cfg.waf_mode == cfg.WAF_ON and filter.mode == F_ON then 124 | log_only = false 125 | end 126 | print('----hit rule, log only is: ', log_only) 127 | log.log_attack(filter.rule_id, rule.desc, rule.attack_tag, 128 | rule.risk_level, filter.action, log_only) 129 | if not log_only then 130 | do_action(filter) 131 | end 132 | end 133 | end 134 | 135 | end 136 | end 137 | 138 | _M.filter = filter 139 | _M.body_filter = body_filter 140 | 141 | return _M 142 | -------------------------------------------------------------------------------- /lua_scripts/module/gzipd.lua: -------------------------------------------------------------------------------- 1 | local zlib = require "zlib.zlib" 2 | local ffi = require "ffi" 3 | 4 | local _M = {} 5 | 6 | local function reader(s) 7 | local done 8 | return function() 9 | if done then return end 10 | done = true 11 | return s 12 | end 13 | end 14 | 15 | local function writer() 16 | local t = {} 17 | return function(data, sz) 18 | if not data then return table.concat(t) end 19 | t[#t + 1] = ffi.string(data, sz) 20 | end 21 | end 22 | 23 | function _M.decompress(gzip_data) 24 | --ngx.log(ngx.ERR, '----raw', gzip_data) 25 | local write = writer() 26 | local format = 'gzip' 27 | zlib.inflate(reader(gzip_data), write, nil, format) 28 | return write() 29 | end 30 | 31 | return _M 32 | -------------------------------------------------------------------------------- /lua_scripts/module/ip_cidr.lua: -------------------------------------------------------------------------------- 1 | local ipairs, tonumber, tostring, type = ipairs, tonumber, tostring, type 2 | local bit = require("bit") 3 | local tobit = bit.tobit 4 | local lshift = bit.lshift 5 | local band = bit.band 6 | local bor = bit.bor 7 | local xor = bit.bxor 8 | local byte = string.byte 9 | local str_find = string.find 10 | local str_sub = string.sub 11 | 12 | local lrucache = nil 13 | 14 | local _M = { 15 | _VERSION = '0.2.1', 16 | } 17 | 18 | local mt = { __index = _M } 19 | 20 | 21 | -- Precompute binary subnet masks... 22 | local bin_masks = {} 23 | for i=1,32 do 24 | bin_masks[tostring(i)] = lshift(tobit((2^i)-1), 32-i) 25 | end 26 | -- ... and their inverted counterparts 27 | local bin_inverted_masks = {} 28 | for i=1,32 do 29 | local i = tostring(i) 30 | bin_inverted_masks[i] = xor(bin_masks[i], bin_masks["32"]) 31 | end 32 | 33 | local log_err 34 | if ngx then 35 | log_err = function(...) 36 | ngx.log(ngx.ERR, ...) 37 | end 38 | else 39 | log_err = function(...) 40 | print(...) 41 | end 42 | end 43 | 44 | 45 | local function enable_lrucache(size) 46 | local size = size or 4000 -- Cache the last 4000 IPs (~1MB memory) by default 47 | local lrucache_obj, err = require("resty.lrucache").new(size) 48 | if not lrucache_obj then 49 | return nil, "failed to create the cache: " .. (err or "unknown") 50 | end 51 | lrucache = lrucache_obj 52 | return true 53 | end 54 | _M.enable_lrucache = enable_lrucache 55 | 56 | 57 | local function split_octets(input) 58 | local pos = 0 59 | local prev = 0 60 | local octs = {} 61 | 62 | for i=1, 4 do 63 | pos = str_find(input, ".", prev, true) 64 | if pos then 65 | if i == 4 then 66 | -- Should not have a match after 4 octets 67 | return nil, "Invalid IP" 68 | end 69 | octs[i] = str_sub(input, prev, pos-1) 70 | elseif i == 4 then 71 | -- Last octet, get everything to the end 72 | octs[i] = str_sub(input, prev, -1) 73 | break 74 | else 75 | return nil, "Invalid IP" 76 | end 77 | prev = pos +1 78 | end 79 | 80 | return octs 81 | end 82 | 83 | 84 | local function ip2bin(ip) 85 | if lrucache then 86 | local get = lrucache:get(ip) 87 | if get then 88 | return get[1], get[2] 89 | end 90 | end 91 | 92 | if type(ip) ~= "string" then 93 | return nil, "IP must be a string" 94 | end 95 | 96 | local octets = split_octets(ip) 97 | if not octets or #octets ~= 4 then 98 | return nil, "Invalid IP" 99 | end 100 | 101 | -- Return the binary representation of an IP and a table of binary octets 102 | local bin_octets = {} 103 | local bin_ip = 0 104 | 105 | for i,octet in ipairs(octets) do 106 | local bin_octet = tonumber(octet) 107 | if not bin_octet or bin_octet > 255 then 108 | return nil, "Invalid octet: "..tostring(octet) 109 | end 110 | bin_octet = tobit(bin_octet) 111 | bin_octets[i] = bin_octet 112 | bin_ip = bor(lshift(bin_octet, 8*(4-i) ), bin_ip) 113 | end 114 | 115 | if lrucache then 116 | lrucache:set(ip, {bin_ip, bin_octets}) 117 | end 118 | return bin_ip, bin_octets 119 | end 120 | _M.ip2bin = ip2bin 121 | 122 | 123 | local function split_cidr(input) 124 | local pos = str_find(input, "/", 0, true) 125 | if not pos then 126 | return {input} 127 | end 128 | return {str_sub(input, 1, pos-1), str_sub(input, pos+1, -1)} 129 | end 130 | 131 | 132 | local function parse_cidr(cidr) 133 | local mask_split = split_cidr(cidr, '/') 134 | local net = mask_split[1] 135 | local mask = mask_split[2] or "32" 136 | local mask_num = tonumber(mask) 137 | if not mask_num or (mask_num > 32 or mask_num < 1) then 138 | return nil, "Invalid prefix: /"..tostring(mask) 139 | end 140 | 141 | local bin_net, err = ip2bin(net) -- Convert IP to binary 142 | if not bin_net then 143 | return nil, err 144 | end 145 | local bin_mask = bin_masks[mask] -- Get masks 146 | local bin_inv_mask = bin_inverted_masks[mask] 147 | 148 | local lower = band(bin_net, bin_mask) -- Network address 149 | local upper = bor(lower, bin_inv_mask) -- Broadcast address 150 | return lower, upper 151 | end 152 | _M.parse_cidr = parse_cidr 153 | 154 | 155 | local function parse_cidrs(cidrs) 156 | local out = {} 157 | local i = 1 158 | for _,cidr in ipairs(cidrs) do 159 | local lower, upper = parse_cidr(cidr) 160 | if not lower then 161 | log_err("Error parsing '", cidr, "': ", upper) 162 | else 163 | out[i] = {lower, upper} 164 | i = i+1 165 | end 166 | end 167 | return out 168 | end 169 | _M.parse_cidrs = parse_cidrs 170 | 171 | 172 | local function ip_in_cidrs(ip, cidrs) 173 | local bin_ip, bin_octets = ip2bin(ip) 174 | if not bin_ip then 175 | return nil, bin_octets 176 | end 177 | 178 | for _,cidr in ipairs(cidrs) do 179 | if bin_ip >= cidr[1] and bin_ip <= cidr[2] then 180 | return true 181 | end 182 | end 183 | return false 184 | end 185 | _M.ip_in_cidrs = ip_in_cidrs 186 | 187 | 188 | local function binip_in_cidrs(bin_ip_ngx, cidrs) 189 | if 4 ~= #bin_ip_ngx then 190 | return false, "invalid IP address" 191 | end 192 | 193 | local bin_ip = 0 194 | for i=1,4 do 195 | bin_ip = bor(lshift(bin_ip, 8), tobit(byte(bin_ip_ngx, i))) 196 | end 197 | 198 | for _,cidr in ipairs(cidrs) do 199 | if bin_ip >= cidr[1] and bin_ip <= cidr[2] then 200 | return true 201 | end 202 | end 203 | return false 204 | end 205 | _M.binip_in_cidrs = binip_in_cidrs 206 | 207 | return _M 208 | -------------------------------------------------------------------------------- /lua_scripts/module/kafka.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- kafka client 3 | local config = require('config') 4 | local cjson= require('cjson') 5 | local producer = require('resty.kafka.producer') 6 | local broker_list = config.kafka_broker_list 7 | 8 | local _M = {} 9 | 10 | local function init() 11 | --[[ 12 | local client = require('resty.kafka.client') 13 | local topic = 'tst_security_waf_attack_log' 14 | local cli = client:new(broker_list) 15 | local brokers, partitions = cli:fetch_metadata(topic) 16 | if not brokers then 17 | ngx.log(ngx.ERR, 'fetch_metadata failed, err:'..partitions) 18 | end 19 | --]] 20 | -- this is async producer_type and bp will be reused in the whole nginx worker 21 | local bp = producer:new(broker_list, { producer_type = "async" }) 22 | if bp == nil then 23 | ngx.log(ngx.ERR, '------new producer error---') 24 | return 25 | end 26 | _M.bp = bp 27 | end 28 | 29 | local function test() 30 | local bp = _M.bp 31 | local topic = 'tst_security_waf_attack_log' 32 | local ok, err = bp:send(topick, "test", "hello") 33 | if not ok then 34 | ngx.log(ngx.ERR, "send err:"..err) 35 | return 36 | end 37 | ngx.say('test successful') 38 | end 39 | 40 | local function send(topic, key, msg) 41 | --[[ 42 | if bp == nil then 43 | ngx.log(ngx.ERR, 'no producer avail.') 44 | return 45 | end 46 | --]] 47 | local bp = _M.bp 48 | if bp then 49 | local ok, err = bp:send(topic, key, msg) 50 | if not ok then 51 | ngx.log(ngx.ERR, "send kafka topic: "..topic.." err: "..err) 52 | return 53 | end 54 | else 55 | ngx.log(ngx.ERR, "get producer error") 56 | end 57 | end 58 | 59 | 60 | _M.init = init 61 | _M.test = test 62 | _M.send = send 63 | 64 | return _M 65 | -------------------------------------------------------------------------------- /lua_scripts/module/libinjection.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local bit = require "bit" 4 | local ffi = require "ffi" 5 | 6 | local b_or = bit.bor 7 | local ffi_load = ffi.load 8 | local ffi_new = ffi.new 9 | local ffi_string = ffi.string 10 | 11 | -- enum sqli_flags 12 | local FLAG_NONE = 0 13 | local FLAG_QUOTE_NONE = 1 14 | local FLAG_QUOTE_SINGLE = 2 15 | local FLAG_QUOTE_DOUBLE = 4 16 | local FLAG_SQL_ANSI = 8 17 | local FLAG_SQL_MYSQL = 16 18 | 19 | -- enum lookup_type 20 | local LOOKUP_FINGERPRINT = 4 21 | 22 | -- enum html5_flags 23 | local DATA_STATE = 0 24 | local VALUE_NO_QUOTE = 1 25 | local VALUE_SINGLE_QUOTE = 2 26 | local VALUE_DOUBLE_QUOTE = 3 27 | local VALUE_BACK_QUOTE = 4 28 | 29 | -- cached b_ors 30 | local QUOTE_NONE_SQL_ANSI = b_or(FLAG_QUOTE_NONE, FLAG_SQL_ANSI) 31 | local QUOTE_NONE_SQL_MYSQL = b_or(FLAG_QUOTE_NONE, FLAG_SQL_MYSQL) 32 | local QUOTE_SINGLE_SQL_ANSI = b_or(FLAG_QUOTE_SINGLE, FLAG_SQL_ANSI) 33 | local QUOTE_SINGLE_SQL_MYSQL = b_or(FLAG_QUOTE_SINGLE, FLAG_SQL_MYSQL) 34 | local QUOTE_DOUBLE_SQL_MYSQL = b_or(FLAG_QUOTE_DOUBLE, FLAG_SQL_MYSQL) 35 | 36 | -- libibjection.so 37 | ffi.cdef[[ 38 | const char* libinjection_sqli_fingerprint(struct libinjection_sqli_state* sql_state, int flags); 39 | 40 | struct libinjection_sqli_token { 41 | char type; 42 | char str_open; 43 | char str_close; 44 | size_t pos; 45 | size_t len; 46 | int count; 47 | char val[32]; 48 | }; 49 | 50 | typedef char (*ptr_lookup_fn)(struct libinjection_sqli_state*, int lookuptype, const char* word, size_t len); 51 | 52 | struct libinjection_sqli_state { 53 | const char *s; 54 | size_t slen; 55 | ptr_lookup_fn lookup; 56 | void* userdata; 57 | int flags; 58 | size_t pos; 59 | struct libinjection_sqli_token tokenvec[8]; 60 | struct libinjection_sqli_token *current; 61 | char fingerprint[8]; 62 | int reason; 63 | int stats_comment_ddw; 64 | int stats_comment_ddx; 65 | int stats_comment_c; 66 | int stats_comment_hash; 67 | int stats_folds; 68 | int stats_tokens; 69 | }; 70 | 71 | void libinjection_sqli_init(struct libinjection_sqli_state * sf, const char *s, size_t len, int flags); 72 | int libinjection_is_sqli(struct libinjection_sqli_state* sql_state); 73 | 74 | int libinjection_sqli(const char* s, size_t slen, char fingerprint[]); 75 | 76 | int libinjection_is_xss(const char* s, size_t len, int flags); 77 | int libinjection_xss(const char* s, size_t slen); 78 | ]] 79 | 80 | _M.version = "0.1" 81 | 82 | local state_type = ffi.typeof("struct libinjection_sqli_state[1]") 83 | local lib, loaded 84 | 85 | -- "borrowed" from CF aho-corasick lib 86 | local function _loadlib() 87 | if (not loaded) then 88 | local path, so_path 89 | local libname = "libinjection.so" 90 | 91 | for k, v in string.gmatch(package.cpath, "[^;]+") do 92 | so_path = string.match(k, "(.*/)") 93 | if so_path then 94 | -- "so_path" could be nil. e.g, the dir path component is "." 95 | so_path = so_path .. libname 96 | 97 | -- Don't get me wrong, the only way to know if a file exist is 98 | -- trying to open it. 99 | local f = io.open(so_path) 100 | if f ~= nil then 101 | io.close(f) 102 | path = so_path 103 | break 104 | end 105 | end 106 | end 107 | 108 | lib = ffi.load(path) 109 | 110 | if (lib) then 111 | loaded = true 112 | return true 113 | else 114 | return false 115 | end 116 | else 117 | return true 118 | end 119 | end 120 | 121 | -- this function is not publicly exposed so we need to emulate it here. not great but not a measurable perf hit 122 | local function _reparse_as_mysql(sqli_state) 123 | return sqli_state[0].stats_comment_ddx ~= 0 or sqli_state[0].stats_comment_hash ~= 0 124 | end 125 | 126 | --[[ 127 | Secondary API: detects SQLi in a string, given a context. Given a string, returns a list of 128 | 129 | * boolean indicating a match 130 | * SQLi fingerprint 131 | --]] 132 | local function _sqli_contextwrapper(string, char, flag1, flag2) 133 | if (char and not string.find(string, char, 1, true)) then 134 | return false, nil 135 | end 136 | 137 | if (not loaded) then 138 | if (not _loadlib()) then 139 | return false, nil 140 | end 141 | end 142 | 143 | local issqli, lookup, sqli_state 144 | 145 | -- allocate a new libinjection_sqli_state struct 146 | sqli_state = ffi.new(state_type) 147 | 148 | -- init the state 149 | lib.libinjection_sqli_init( 150 | sqli_state, 151 | string, 152 | #string, 153 | FLAG_NONE 154 | ) 155 | 156 | -- initial fingerprint 157 | lib.libinjection_sqli_fingerprint( 158 | sqli_state, 159 | flag1 160 | ) 161 | 162 | -- lookup 163 | lookup = sqli_state[0].lookup( 164 | sqli_state, 165 | LOOKUP_FINGERPRINT, 166 | sqli_state[0].fingerprint, 167 | #ffi.string(sqli_state[0].fingerprint) 168 | ) 169 | 170 | -- match? great, we're done 171 | if (lookup > 0) then 172 | return true, ffi_string(sqli_state[0].fingerprint) 173 | end 174 | 175 | -- no? reparse, fingerprint and lookup again 176 | if (flag2 and _reparse_as_mysql(sqli_state)) then 177 | lib.libinjection_sqli_fingerprint( 178 | sqli_state, 179 | flag2 180 | ) 181 | 182 | lookup = sqli_state[0].lookup( 183 | sqli_state, 184 | LOOKUP_FINGERPRINT, 185 | sqli_state[0].fingerprint, 186 | #ffi.string(sqli_state[0].fingerprint) 187 | ) 188 | 189 | if (lookup > 0) then 190 | return true, ffi_string(sqli_state[0].fingerprint) 191 | end 192 | end 193 | 194 | return false, nil 195 | end 196 | 197 | --[[ 198 | Wrapper for second-level API with no char context 199 | --]] 200 | function _M.sqli_noquote(string) 201 | return _sqli_contextwrapper( 202 | string, 203 | nil, 204 | QUOTE_NONE_SQL_ANSI, 205 | QUOTE_NONE_SQL_MYSQL 206 | ) 207 | end 208 | 209 | --[[ 210 | Wrapper for second-level API with CHAR_SINGLE context 211 | --]] 212 | function _M.sqli_singlequote(string) 213 | return _sqli_contextwrapper( 214 | string, 215 | "'", 216 | QUOTE_SINGLE_SQL_ANSI, 217 | QUOTE_SINGLE_SQL_MYSQL 218 | ) 219 | end 220 | 221 | --[[ 222 | Wrapper for second-level API with CHAR_DOUBLE context 223 | --]] 224 | function _M.sqli_doublequote(string) 225 | return _sqli_contextwrapper( 226 | string, 227 | '"', 228 | QUOTE_DOUBLE_SQL_MYSQL 229 | ) 230 | end 231 | 232 | --[[ 233 | Simple API. Given a string, returns a list of 234 | 235 | * boolean indicating a match 236 | * SQLi fingerprint 237 | --]] 238 | function _M.sqli(string) 239 | if (not loaded) then 240 | if (not _loadlib()) then 241 | ngx.log(ngx.ERR,'------fuck libinjection') 242 | return false, nil 243 | end 244 | end 245 | 246 | local fingerprint = ffi_new("char [8]") 247 | 248 | return lib.libinjection_sqli(string, #string, fingerprint) == 1, ffi_string(fingerprint) 249 | end 250 | 251 | --[[ 252 | Secondary API: detects XSS in a string, given a context. Given a string, returns a boolean denoting if XSS was detected 253 | --]] 254 | local function _xss_contextwrapper(string, flag) 255 | if (not loaded) then 256 | if (not _loadlib()) then 257 | return false 258 | end 259 | end 260 | 261 | return lib.libinjection_is_xss(string, #string, flag) == 1 262 | end 263 | 264 | --[[ 265 | Wrapper for second-level API with DATA_STATE flag 266 | --]] 267 | function _M.xss_data_state(string) 268 | return _xss_contextwrapper( 269 | string, 270 | DATA_STATE 271 | ) 272 | end 273 | 274 | --[[ 275 | Wrapper for second-level API with VALUE_NO_QUOTE flag 276 | --]] 277 | function _M.xss_noquote(string) 278 | return _xss_contextwrapper( 279 | string, 280 | VALUE_NO_QUOTE 281 | ) 282 | end 283 | 284 | --[[ 285 | Wrapper for second-level API with VALUE_SINGLE_QUOTE flag 286 | --]] 287 | function _M.xss_singlequote(string) 288 | return _xss_contextwrapper( 289 | string, 290 | VALUE_SINGLE_QUOTE 291 | ) 292 | end 293 | 294 | --[[ 295 | Wrapper for second-level API with VALUE_DOUBLE_QUOTE flag 296 | --]] 297 | function _M.xss_doublequote(string) 298 | return _xss_contextwrapper( 299 | string, 300 | VALUE_DOUBLE_QUOTE 301 | ) 302 | end 303 | 304 | --[[ 305 | Wrapper for second-level API with VALUE_BACK_QUOTE flag 306 | --]] 307 | function _M.xss_backquote(string) 308 | return _xss_contextwrapper( 309 | string, 310 | VALUE_BACK_QUOTE 311 | ) 312 | end 313 | 314 | --[[ 315 | ALPHA version of XSS detector. Given a string, returns a boolean denoting if XSS was detected 316 | --]] 317 | function _M.xss(string) 318 | if (not loaded) then 319 | if (not _loadlib()) then 320 | return false 321 | end 322 | end 323 | 324 | return lib.libinjection_xss(string, #string) == 1 325 | end 326 | 327 | return _M 328 | -------------------------------------------------------------------------------- /lua_scripts/module/load_ac.lua: -------------------------------------------------------------------------------- 1 | -- Helper wrappring script for loading shared object libac.so (FFI interface) 2 | -- from package.cpath instead of LD_LIBRARTY_PATH. 3 | -- 4 | 5 | local ffi = require 'ffi' 6 | ffi.cdef[[ 7 | void* ac_create(const char** str_v, unsigned int* strlen_v, 8 | unsigned int v_len); 9 | int ac_match2(void*, const char *str, int len); 10 | void ac_free(void*); 11 | ]] 12 | 13 | local _M = {} 14 | 15 | local string_gmatch = string.gmatch 16 | local string_match = string.match 17 | 18 | local ac_lib = nil 19 | local ac_create = nil 20 | local ac_match = nil 21 | local ac_free = nil 22 | 23 | --[[ Find shared object file package.cpath, obviating the need of setting 24 | LD_LIBRARY_PATH 25 | ]] 26 | local function find_shared_obj(cpath, so_name) 27 | for k, v in string_gmatch(cpath, "[^;]+") do 28 | local so_path = string_match(k, "(.*/)") 29 | if so_path then 30 | -- "so_path" could be nil. e.g, the dir path component is "." 31 | so_path = so_path .. so_name 32 | 33 | -- Don't get me wrong, the only way to know if a file exist is 34 | -- trying to open it. 35 | local f = io.open(so_path) 36 | if f ~= nil then 37 | io.close(f) 38 | return so_path 39 | end 40 | end 41 | end 42 | end 43 | 44 | function _M.load_ac_lib() 45 | if ac_lib ~= nil then 46 | return ac_lib 47 | else 48 | local so_path = find_shared_obj(package.cpath, "libac.so") 49 | if so_path ~= nil then 50 | ac_lib = ffi.load(so_path) 51 | ac_create = ac_lib.ac_create 52 | ac_match = ac_lib.ac_match2 53 | ac_free = ac_lib.ac_free 54 | return ac_lib 55 | end 56 | end 57 | end 58 | 59 | -- Create an Aho-Corasick instance, and return the instance if it was 60 | -- successful. 61 | function _M.create_ac(dict) 62 | print('---ac dict, ', ', type', type(dict), ', value', tostring(dict)) 63 | local strnum = #dict 64 | if ac_lib == nil then 65 | _M.load_ac_lib() 66 | end 67 | 68 | local str_v = ffi.new("const char *[?]", strnum) 69 | local strlen_v = ffi.new("unsigned int [?]", strnum) 70 | 71 | for i = 1, strnum do 72 | local s = dict[i] 73 | str_v[i - 1] = s 74 | strlen_v[i - 1] = #s 75 | end 76 | 77 | local ac = ac_create(str_v, strlen_v, strnum); 78 | if ac ~= nil then 79 | return ffi.gc(ac, ac_free) 80 | end 81 | end 82 | 83 | -- Return nil if str doesn't match the dictionary, else return non-nil. 84 | function _M.match(ac, str) 85 | local r = ac_match(ac, str, #str); 86 | if r >= 0 then 87 | return r 88 | end 89 | end 90 | 91 | return _M 92 | -------------------------------------------------------------------------------- /lua_scripts/module/log.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- 调用log_attack保存攻击类型和攻击日志信息 3 | 4 | local cjson = require('cjson') 5 | local cfg = require('config') 6 | local configs = cfg.configs 7 | local is_micro = cfg.is_micro 8 | local utils = require('utils') 9 | local logger = require "resty.logger.socket" 10 | 11 | 12 | local kafka = require('kafka') 13 | local ngx_topic = cfg.kafka_topics.ngx_topic 14 | local ngx_key = 'ngx_access_log|ngx_access_log|all|day|%d' 15 | local waf_topic = cfg.kafka_topics.waf_topic 16 | local waf_key = 'waf_log|waf_log|all|day|%d' 17 | 18 | local _M = {} 19 | local function init_syslogger() 20 | if not logger.initted() then 21 | local ok, err = logger.init{ 22 | host = cfg.syslog_host, 23 | port = cfg.syslog_port, 24 | sock_type = cfg.syslog_sock_type or "tcp", 25 | flush_limit = cfg.syslog_flush_limit or 1000, 26 | drop_limit = cfg.syslog_drop_limit or 2000 27 | } 28 | if not ok then 29 | ngx.log(ngx.ERR, "-----failed to initialize the logger: ", 30 | err) 31 | return 32 | else 33 | print("logger init ok:", ok, ", error", err) 34 | end 35 | end 36 | 37 | end 38 | 39 | local log_type = cfg['log_type'] or 'syslog' 40 | if cfg.log_type == 'syslog' then 41 | init_syslogger() 42 | end 43 | 44 | local function get_server_addr() 45 | local KEY = "server_addr" 46 | local cache_cfg = ngx.shared.cfg 47 | local server_addr = cache_cfg:get(KEY) 48 | if server_addr == nil then 49 | server_addr = ngx.var.server_addr 50 | if server_addr then 51 | cache_cfg:set(KEY, server_addr, 0) 52 | end 53 | end 54 | return server_addr 55 | end 56 | 57 | local function log_attack(id, desc, tag, risk_level, action, log_only) 58 | --保存本次请求的攻击log信息 rule_id, rule_desc, rule_attack_tag, rule_risk_level, action 59 | local attack_log = { 60 | rule_id = id, 61 | rule_desc = desc, 62 | attack_tag = tag, 63 | risk_level = risk_level, 64 | action = action, 65 | log_only = log_only 66 | } 67 | ngx.ctx.attack_log = attack_log 68 | end 69 | 70 | 71 | 72 | local function write_file(filename, msg) 73 | local fd = io.open(filename, "ab") 74 | if fd == nil then 75 | return 76 | end 77 | fd:write(msg) 78 | fd:flush() 79 | fd:close() 80 | end 81 | 82 | local function send_to_logserver() 83 | --输出到log server 84 | local http = require "resty.http" 85 | local client = http.new() 86 | 87 | local log_server_uri = 'http://10.156.247.0/receiver/sendSecurity' 88 | local timeout = 10 --请求log服务器超时时间 89 | 90 | client:set_timeout() 91 | client:request_uri(log_server_uri) 92 | 93 | end 94 | 95 | local function write_log(msg) 96 | local logs_dir = configs.logs_dir 97 | -- logs_dir一定要以'\'或'/'结尾,否则文件路径不对 98 | if logs_dir then 99 | local server = ngx.var.server_name 100 | local filename = logs_dir..server..'_'..ngx.today()..'_sec.log' 101 | write_file(filename, table.concat(msg, ' ')) 102 | else 103 | ngx.log(ngx.ERR, '----no logs dir'..msg) 104 | end 105 | end 106 | 107 | local function log_attack_to_kafka(msg) 108 | local _key = waf_key:format(ngx.now()) 109 | kafka.send(waf_topic, _key, cjson.encode(msg)) 110 | end 111 | 112 | local function log_access_to_kafka(msg) 113 | local _key = ngx_key:format(ngx.now()) 114 | kafka.send(ngx_topic, _key, cjson.encode(msg)) 115 | end 116 | 117 | local function log_to_syslog(msg) 118 | -- construct the custom access log message in 119 | -- the Lua variable "msg" 120 | local msg = cjson.encode(msg) 121 | local bytes, err = logger.log(msg) 122 | if err then 123 | ngx.log(ngx.ERR, "-------failed to log message: ", err) 124 | return 125 | else 126 | print("logger log bytes:", bytes, ", error", err) 127 | end 128 | end 129 | 130 | local function _log_access(msg) 131 | if log_type == 'kafka' then 132 | log_access_to_kafka(msg) 133 | else 134 | log_to_syslog(msg) 135 | end 136 | end 137 | 138 | local function _log_attack(msg) 139 | if log_type == 'kafka' then 140 | log_attack_to_kafka(msg) 141 | else 142 | log_to_syslog(msg) 143 | end 144 | end 145 | 146 | local function do_log() 147 | local log_info = { 148 | status = tonumber(ngx.var.status), 149 | body_bytes_sent = tonumber(ngx.var.body_bytes_sent) or 0, 150 | scheme = ngx.var.scheme, 151 | http_user_agent = ngx.var.http_user_agent, 152 | request_length = tonumber(ngx.var.request_length) or 0, 153 | request_body = ngx.var.request_body, 154 | remote_addr = ngx.var.remote_addr, 155 | request_time = tonumber(ngx.var.request_time) or 0, 156 | host = ngx.var.host, 157 | server_port = ngx.var.server_port, 158 | clientIp = utils.get_client_ip(), 159 | request_uri = ngx.var.request_uri, 160 | time = ngx.localtime(), 161 | http_referer = ngx.var.http_referer, 162 | x_forwarded_for = ngx.var.http_x_forwarded_for, 163 | x_real_ip = ngx.var.http_x_real_ip, 164 | upstream_addr = ngx.var.upstream_addr, 165 | method = ngx.req.get_method(), 166 | server_addr = get_server_addr(), 167 | is_micro = is_micro, 168 | appName = ngx.var.host, 169 | nid = ngx.var.nid or '', 170 | user_id = ngx.var.user_id or '', 171 | source = 'ngxAccess' 172 | } 173 | -- log nginx access 174 | _log_access(log_info) 175 | 176 | -- if has an attack 177 | local attack_log = ngx.ctx.attack_log 178 | --log attack 179 | if type(attack_log) == 'table' then 180 | for k, v in pairs(attack_log) do 181 | log_info[k] = v 182 | end 183 | log_info['source'] = 'wafAttack' 184 | _log_attack(log_info) 185 | end 186 | end 187 | 188 | _M.write_file = write_file 189 | _M.log_attack = log_attack 190 | _M.do_log = do_log 191 | 192 | return _M 193 | -------------------------------------------------------------------------------- /lua_scripts/module/operators.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | 3 | local ip_cidr = require('ip_cidr') 4 | local utils = require('utils') 5 | local transform = require('transform') 6 | local black_list = require('black_list') 7 | local libinject = require('libinjection') 8 | local cjson = require('cjson') 9 | --local ac = require('ahocorasick') 10 | local ac = require('load_ac') 11 | 12 | local _M = {} 13 | local _pcre_flags = 'ioj' 14 | 15 | local function _starts_with(str, start) 16 | if type (start) == 'string' then 17 | return str:sub(1, #start) == start 18 | end 19 | end 20 | 21 | local function _ends_with(str, ending) 22 | if type (ending) == 'string' then 23 | return ending == "" or str:sub(-#ending) == ending 24 | end 25 | end 26 | 27 | function _M.starts_with (i, r) 28 | local starts, value 29 | if type (i) == 'table' then 30 | for _, v in ipairs (i) do 31 | starts, value = _M.starts_with (v, r) 32 | if starts then 33 | break 34 | end 35 | end 36 | else 37 | if type(r) == 'table' then 38 | for _, v in ipairs (r) do 39 | starts = _starts_with(i, v) 40 | if starts then 41 | value = v 42 | break 43 | end 44 | end 45 | end 46 | end 47 | return starts, value 48 | end 49 | 50 | function _M.ends_with (i, r) 51 | local ends, value 52 | if type (i) == 'table' then 53 | for _, v in ipairs (i) do 54 | ends, value = _M.ends_with (v, r) 55 | if ends then 56 | break 57 | end 58 | end 59 | else 60 | if type(r) == 'table' then 61 | for _, v in ipairs (r) do 62 | ends = _ends_with(i, v) 63 | if ends then 64 | value = v 65 | break 66 | end 67 | end 68 | end 69 | end 70 | return ends, value 71 | end 72 | 73 | 74 | local _ac_dicts = {} 75 | local function _get_ac_dict(dict_strs) 76 | local rule_id = ngx.ctx.rule_id 77 | if (not _ac_dicts[rule_id]) then 78 | _ac = ac.create_ac(dict_strs) 79 | _ac_dicts[rule_id] = _ac 80 | else 81 | _ac = _ac_dicts[rule_id] 82 | end 83 | return _ac 84 | end 85 | 86 | function _M.detect_sqli(input) 87 | if (type(input) == 'table') then 88 | for _, v in ipairs(input) do 89 | local match, value = _M.detect_sqli(v) 90 | 91 | if match then 92 | return true, value 93 | end 94 | end 95 | else 96 | -- yes this is really just one line 97 | -- libinjection.sqli has the same return values that lookup.operators expects 98 | if type(input) == 'string' then 99 | return libinject.sqli(tostring(input)) 100 | else 101 | return false, nil 102 | end 103 | end 104 | 105 | return false, nil 106 | end 107 | 108 | function _M.detect_xss(input) 109 | if (type(input) == 'table') then 110 | for _, v in ipairs(input) do 111 | local match, value = _M.detect_xss(v) 112 | 113 | if match then 114 | return match, value 115 | end 116 | end 117 | else 118 | -- this function only returns a boolean value 119 | -- so we'll wrap the return values ourselves 120 | if type(input) == 'string' and (libinject.xss(input)) then 121 | return true, input 122 | else 123 | return false, nil 124 | end 125 | end 126 | 127 | return false, nil 128 | end 129 | 130 | -- i is input value, r is defined rule value, returned value means which value in i (case table) 131 | function _M.equal(i, r) 132 | local eq, value 133 | if (type (i) == "table") then 134 | for _, v in ipairs (i) do 135 | eq, value = _M.equal (v, r) 136 | if eq then 137 | break 138 | end 139 | end 140 | else 141 | eq = i == r 142 | if eq then 143 | value = i 144 | end 145 | end 146 | return eq, value 147 | end 148 | 149 | function _M.not_equal(i, r) 150 | local result = _M.equal (i, r) 151 | return not result 152 | end 153 | 154 | function _M.greater_than(i, r) 155 | local gt, value 156 | if (type (i) == "table") then 157 | for _, v in ipairs (i) do 158 | gt, value = _M.greater_than (v, r) 159 | if gt then 160 | break 161 | end 162 | end 163 | else 164 | gt = i > r 165 | if gt then 166 | value = i 167 | end 168 | end 169 | return gt, value 170 | end 171 | 172 | function _M.less_than(i, r) 173 | local lt, value 174 | if (type (i) == "table") then 175 | for _, v in ipairs (i) do 176 | lt, value = _M.less_than (v, r) 177 | if lt then 178 | break 179 | end 180 | end 181 | else 182 | lt = i < r 183 | if lt then 184 | value = i 185 | end 186 | end 187 | return lt, value 188 | end 189 | 190 | function _M.greater_than_equal(i, r) 191 | local gte, value 192 | if (type (i) == "table") then 193 | for _, v in ipairs (i) do 194 | gte, value = _M.greater_than_equal (v, r) 195 | if gte then 196 | break 197 | end 198 | end 199 | else 200 | gte = i >= r 201 | if gte then 202 | value = i 203 | end 204 | end 205 | return gte, value 206 | end 207 | 208 | function _M.less_than_equal(i, r) 209 | local lte, value 210 | if (type (i) == "table") then 211 | for _, v in ipairs (i) do 212 | lte, value = _M.less_than_equal (v, r) 213 | if lte then 214 | break 215 | end 216 | end 217 | else 218 | lte = i <= r 219 | if lte then 220 | value = i 221 | end 222 | end 223 | return lte, value 224 | end 225 | 226 | function _M.any() 227 | return true 228 | end 229 | 230 | function _M.not_any() 231 | return false 232 | end 233 | 234 | function _M.exists(i) 235 | local ex, value 236 | if type (i) == "table" then 237 | for _, v in ipairs (i) do 238 | ex, value = _M.exists (v) 239 | if ex then 240 | break 241 | end 242 | end 243 | else 244 | ex = i ~= nil 245 | if ex then 246 | value = i 247 | end 248 | end 249 | return ex, value 250 | end 251 | 252 | function _M.not_exists(i) 253 | local result = _M.exists (i) 254 | return not result 255 | end 256 | function _M.regex_wrap(i, r) 257 | -- rule value is base64 encode 258 | local rule_value = transform.lookup.base64_decode(r) 259 | return _M.regex(i, rule_value) 260 | end 261 | 262 | function _M.regex(i, r) 263 | local from, to, err, match 264 | if type(i) == "table" then 265 | for _, v in ipairs(i) do 266 | match, from = _M.regex(v, r) 267 | if match then 268 | break 269 | end 270 | end 271 | else 272 | from, to, err = ngx.re.find(i, r, _pcre_flags) 273 | if err then 274 | ngx.log(ngx.WARN, "error in ngx.re.find: " .. err) 275 | end 276 | 277 | if from then 278 | match = true 279 | end 280 | end 281 | 282 | return match, from 283 | end 284 | 285 | function _M.not_regex_wrap(i, r) 286 | return not _M.regex_wrap(i, r) 287 | end 288 | 289 | -- aho-corasick字符串多模匹配 290 | function _M.contains(i, r) 291 | print('-----contains', ', input', i, ', rule value', r) 292 | local _ac = _get_ac_dict(r) 293 | return type(i) == 'string' and ac.match(_ac, i) 294 | end 295 | 296 | function _M.not_contains(i, r) 297 | return not _M.contains(i, r) 298 | end 299 | 300 | function _M.did_filter(i) 301 | return black_list.did_filter(i) 302 | end 303 | 304 | local _cidr_cache = {} 305 | function _M.cidr(i, r) 306 | local t = {} 307 | local n = 1 308 | 309 | if (type(r) ~= "table") then 310 | r = { r } 311 | end 312 | 313 | for _, v in ipairs(r) do 314 | local cidr = _cidr_cache[v] 315 | -- if it wasn't there, compute and cache the value 316 | if (not cidr) then 317 | local lower, upper = ip_cidr.parse_cidr(v) 318 | cidr = { lower, upper } 319 | _cidr_cache[v] = cidr 320 | end 321 | 322 | t[n] = cidr 323 | n = n + 1 324 | end 325 | 326 | return ip_cidr.ip_in_cidrs(i, t), i 327 | end 328 | 329 | 330 | _M.lookup = { 331 | EQL = function (i, r) return _M.equal(i, r) end, 332 | NEQ = function (i, r) return _M.not_equal(i, r) end, 333 | GET = function (i, r) return _M.greater_than(i, r) end, 334 | GTE = function (i, r) return _M.greater_than_equal(i, r) end, 335 | LET = function (i, r) return _M.less_than(i, r) end, 336 | LTE = function (i, r) return _M.less_than_equal(i, r) end, 337 | REG = function (i, r) return _M.regex_wrap(i, r) end, 338 | NRE = function (i, r) return _M.not_regex_wrap(i, r) end, 339 | DID = function (i, r) return _M.did_filter(i) end, 340 | SQL = function (i, r) return _M.detect_sqli(i) end, 341 | XSS = function (i, r) return _M.detect_xss(i) end, 342 | ANY = function (i, r) return _M.any() end, 343 | NAN = function (i, r) return _M.not_any() end, 344 | EXT = function (i, r) return _M.exists(i) end, 345 | NEX = function (i, r) return _M.not_exists(i) end, 346 | STW = function (i, r) return _M.starts_with (i, r) end, 347 | EDW = function (i, r) return _M.ends_with (i, r) end, 348 | CDR = function (i, r) return _M.cidr (i, r) end, 349 | CTN = function (i, r) return _M.contains (i, r) end, 350 | NCT = function (i, r) return _M.not_contains (i, r) end, 351 | } 352 | 353 | return _M 354 | -------------------------------------------------------------------------------- /lua_scripts/module/request.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local upload = require "resty.upload" 3 | local _M = {} 4 | local utils = require('utils') 5 | local cookie = require "cookie" 6 | local ACCESS_ON = 1 7 | local _pcre_flags = 'ioj' 8 | local _truncated = 1 9 | 10 | local function _parse_uri_args() 11 | local args, err = ngx.req.get_uri_args() 12 | if err == "truncated" then 13 | -- one can choose to ignore or reject the current request here 14 | ngx.log(ngx.CRIT, 'exceeds MAXIMUM 100 uri args') 15 | ngx.ctx.uri_args_truncated = _truncated 16 | end 17 | return args 18 | end 19 | 20 | local function _parse_body_args() 21 | local body_access = ngx.ctx.host_cfg.request_body_access 22 | if body_access ~= ACCESS_ON then 23 | return 24 | end 25 | local _body = ngx.ctx.request_body 26 | if _body then 27 | return _body 28 | end 29 | ngx.log(ngx.CRIT, 'should exec *** ONCE *** per request') 30 | ngx.req.read_body() 31 | local args, err = ngx.req.get_post_args() 32 | if err == "truncated" then 33 | -- one can choose to ignore or reject the current request here 34 | ngx.log(ngx.CRIT, 'exceeds MAXIMUM 100 body args') 35 | ngx.ctx.body_args_truncated = _truncated 36 | end 37 | if not args then 38 | print("failed to get request body: ", err) 39 | return 40 | end 41 | ngx.ctx.request_body = args 42 | return args 43 | end 44 | 45 | local function _parse_cookies() 46 | local cookie_obj, err = cookie:new() 47 | return cookie_obj:get_all() or {} 48 | end 49 | local function _parse_headers() 50 | --local headers, err = ngx.req.get_headers(0) 51 | local headers, err = ngx.req.get_headers() 52 | if err == "truncated" then 53 | -- one can choose to ignore or reject the current request here 54 | ngx.log(ngx.CRIT, 'exceeds MAXIMUM 100 headers') 55 | ngx.ctx.req_headers_truncated = _truncated 56 | end 57 | if not headers then 58 | print("failed to get request headers: ", err) 59 | return 60 | end 61 | return headers 62 | end 63 | 64 | local function _get_nofile_size() 65 | local _body = ngx.ctx.request_body_size 66 | if _body then 67 | return _body 68 | end 69 | local content_type = ngx.req.get_headers()["content-type"] 70 | if not content_type or not ngx.re.find(content_type, [=[^application/x-www-form-urlencoded]=], _pcre_flags) then 71 | return 72 | end 73 | ngx.req.read_body() 74 | local body = ngx.req.get_body_data() 75 | if body then 76 | _body = #body 77 | ngx.ctx.request_body_size = _body 78 | print('get request nofile size: ', _body, ', body: ', body) 79 | return _body 80 | end 81 | end 82 | 83 | local function _parse_upload_file() 84 | local upload_file_access = ngx.ctx.host_cfg.upload_file_access 85 | local upload_file_limit = ngx.ctx.host_cfg.request_body_limit 86 | if upload_file_access ~= ACCESS_ON then 87 | return 88 | end 89 | local content_type = ngx.req.get_headers()["content-type"] 90 | print('upload file content type: ', content_type) 91 | if not content_type or not ngx.re.find(content_type, [=[^multipart/form-data; boundary=]=], _pcre_flags) then 92 | return 93 | end 94 | local chunk_size = 4096 -- should be set to 4096 or 8192 for real-world settings 95 | local file_size = 0 96 | local form, err = upload:new(chunk_size) 97 | if not form then 98 | ngx.log(ngx.ERR, "failed to new upload: ", err) 99 | return 100 | end 101 | form:set_timeout(1000) -- 1 sec 102 | local FILES_NAMES = {} 103 | while true do 104 | local typ, res, err = form:read() 105 | if not typ then 106 | ngx.log(ngx.ERR, "failed to stream request body: ", err) 107 | return 108 | end 109 | if typ == "header" then 110 | if res[1]:lower() == 'content-disposition' then 111 | local header = res[2] 112 | local s, f = header:find(' name="([^"]+")') 113 | file = header:sub(s + 7, f - 1) 114 | table.insert(FILES_NAMES, file) 115 | s, f = header:find('filename="([^"]+")') 116 | if s then table.insert(FILES, header:sub(s + 10, f - 1)) 117 | end 118 | end 119 | end 120 | print('upload file body type: ', typ, ', res type: ', type(res)) 121 | if type(res) == 'string' then 122 | file_size = file_size + #res 123 | end 124 | if typ == "eof" then 125 | break 126 | end 127 | if file_size > upload_file_limit then 128 | print('exceeds upload file limit, return instant', file_size, ':', upload_file_limit) 129 | break 130 | end 131 | end 132 | print('get request upload file names', cjson.encode(FILES_NAMES)) 133 | ngx.ctx.upload_file_names = FILES_NAMES 134 | print('get request upload file size: ', file_size) 135 | return file_size 136 | end 137 | 138 | local function _get_upload_file_size() 139 | local file_size = _parse_upload_file() or 0 140 | return file_size 141 | end 142 | 143 | local function _parse_body_headers() 144 | local headers, err = ngx.resp.get_headers() 145 | 146 | if err == "truncated" then 147 | -- one can choose to ignore or reject the current response here 148 | ngx.log(ngx.CRIT, 'exceeds MAXIMUM 100 headers') 149 | ngx.ctx.resp_headers_truncated = _truncated 150 | end 151 | if not headers then 152 | print("failed to get response headers: ", err) 153 | return 154 | end 155 | return headers 156 | end 157 | 158 | 159 | _M.lookup = { 160 | body_args = function() return _parse_body_args() end, 161 | body_nofile_size = function() return _get_nofile_size() end, 162 | upload_file_size = function() return _get_upload_file_size() end, 163 | uri_args = function() return _parse_uri_args() end, 164 | uri = function() return ngx.req.uri end, 165 | uri_args_size = function() return #ngx.var.args end, 166 | method = function() return ngx.req.get_method() end, 167 | ip = function() return utils.get_client_ip() end, 168 | host = function() return ngx.var.host end, 169 | cookies = function() return _parse_cookies() end, 170 | ua = function() return ngx.var.http_user_agent end, 171 | refer = function() return ngx.var.http_referer end, 172 | headers = function() return _parse_headers() end, 173 | body_headers = function() return _parse_body_headers() end 174 | } 175 | return _M 176 | -------------------------------------------------------------------------------- /lua_scripts/module/rule.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- rule engine 3 | local _M = {} 4 | local ops = require('operators') 5 | local request = require('request') 6 | local utils = require('utils') 7 | local transform = require('transform') 8 | local cjson = require('cjson') 9 | 10 | local function _get_items_value(items) 11 | local ls = {} 12 | for _, item in ipairs(items) do 13 | local lm = _M.lookup [item] 14 | if not lm then 15 | print ('invalid item: ', item) 16 | break 17 | end 18 | local result = lm() 19 | print ('get item: ', item, ', value: ', result) 20 | if type (result) == 'table' and #result > 0 then 21 | print('items input values: ', cjson.encode(result)) 22 | ls[#ls+1] = result 23 | else 24 | ls [#ls+1] = result 25 | end 26 | end 27 | print('items values: ', cjson.encode(ls)) 28 | return ls 29 | end 30 | 31 | local function _value_transform(value, t) 32 | if type(value) == 'table' and next(value) ~= nil then 33 | for k, v in ipairs(value) do 34 | for _, tr in ipairs(t) do 35 | print('value to be tranform: ', v) 36 | print('tranform: ', tr) 37 | value[k] = transform.lookup[tr](v) 38 | print('after tranform: ', v) 39 | end 40 | end 41 | return value 42 | else 43 | for _, tr in ipairs(t) do 44 | print('value to be tranform: ', value) 45 | print('tranform: ', tr) 46 | value = transform.lookup[tr](value) 47 | print('after tranform: ', value) 48 | end 49 | return value 50 | end 51 | end 52 | -- m, one in rule match list, || 53 | local function _match(m) 54 | op = m['operator'] 55 | input = _get_items_value(m['items']) 56 | t = m['transforms'] 57 | rule_value = m['value'] 58 | print('input: ', cjson.encode(input), ', op: ', op, ', rule value: ', tostring(rule_value)) 59 | for _, item in ipairs(input) do 60 | if t then 61 | print('start transform') 62 | item = _value_transform(item, t) 63 | end 64 | print('******item: ', item, ', op: ', op, ', rule value: ', tostring(rule_value)) 65 | matched, value = ops.lookup[op](item, rule_value) 66 | print('match result: ', matched, ', matched value: ', value) 67 | if matched then 68 | return true 69 | end 70 | end 71 | end 72 | 73 | -- matcher, match list in rule, && 74 | function _M.match(matcher) 75 | if matcher == nil or type(matcher) ~= 'table' or next(matcher) == nil then 76 | return false 77 | end 78 | for _, m in ipairs(matcher) do 79 | if _match(m) ~= true then 80 | print('not match rule condition ', _) 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | _M.lookup = { 88 | ALL_ARGS = function() return utils.table_values(request.lookup.uri_args()) end, 89 | URI = function() return request.lookup.uri() end, 90 | ALL_ARGS_NAMES = function() return utils.table_keys(request.lookup.uri_args()) end, 91 | ALL_ARGS_COMBINED_SIZE = function() return request.lookup.uri_args_size() end, 92 | BODY_ARGS = function() return utils.table_values(request.lookup.body_args()) end, 93 | BODY_ARGS_NAMES = function() return utils.table_keys(request.lookup.body_args()) end, 94 | METHOD = function() return request.lookup.method() end, 95 | HOST = function() return request.lookup.host() end, 96 | UserAgent = function() return request.lookup.ua end, 97 | REFER = function() return request.lookup.refer end, 98 | IP = function() return request.lookup.ip() end, 99 | HEADERS = function() return utils.table_values(request.lookup.headers()) end, 100 | HEADERS_NAMES = function() return utils.table_keys(request.lookup.headers()) end, 101 | COOKIES = function() return utils.table_values(request.lookup.cookies()) end, 102 | COOKIES_NAMES = function() return utils.table_keys(request.lookup.cookies()) end, 103 | RESPONSE_STATUS = function () return ngx.var.status end, 104 | RESPONSE_BODY = function () return ngx.ctx.response_body end, 105 | -- 可通过加转换length来达到 106 | -- RESPONSE_BODY_LENGTH = function () return #ngx.ctx.response_body end, 107 | RESPONSE_HEADERS = function () return utils.table_values(request.lookup.body_headers ()) end, 108 | RESPONSE_HEADERS_NAMES = function () return utils.table_keys(request.lookup.body_headers ()) end, 109 | -- request protocol, usually “HTTP/1.0”, “HTTP/1.1”, or “HTTP/2.0” 110 | RESPONSE_PROTOCOL = function () return ngx.var.server_protocol end, 111 | UPLOAD_FILE_SIZE = function () return request.lookup.upload_file_size() end, 112 | UPLOAD_FILE_NAMES = function () return ngx.ctx.upload_file_names end, 113 | URI_ARGS_OF = function () return ngx.ctx.uri_args_truncated or 0 end, 114 | BODY_ARGS_OF = function () return ngx.ctx.body_args_truncated or 0 end, 115 | REQ_HEADERS_OF = function () return ngx.ctx.req_headers_truncated or 0 end, 116 | RESP_HEADERS_OF = function () return ngx.ctx.resp_headers_truncated or 0 end, 117 | BODY_NOFILE_SIZE = function () return request.lookup.body_nofile_size() end 118 | } 119 | 120 | return _M 121 | 122 | -------------------------------------------------------------------------------- /lua_scripts/module/transform.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | local _pcre_flags = 'ioj' 3 | 4 | 5 | _M.lookup = { 6 | compress_whitespace = function(value) 7 | local str = tostring(value) 8 | return ngx.re.gsub(str, [=[\s+]=], ' ', _pcre_flags) 9 | end, 10 | length = function(value) 11 | return string.len(tostring(value)) 12 | end, 13 | count = function(value) 14 | if type(value) == 'table' then 15 | return #value 16 | else 17 | return string.len(tostring(value)) 18 | end 19 | end, 20 | hex_decode = function(value) 21 | local value 22 | if (pcall(function() 23 | value = str:gsub('..', function (cc) 24 | return string.char(tonumber(cc, 16)) 25 | end) 26 | end)) then 27 | return value 28 | else 29 | return str 30 | end 31 | end, 32 | hex_encode = function(value) 33 | return (value:gsub('.', function (c) 34 | return string_format('%02x', string_byte(c)) 35 | end)) 36 | end, 37 | html_decode = function(value) 38 | local str = ngx.re.gsub(value, [=[<]=], '<', _pcre_flags) 39 | str = ngx.re.gsub(str, [=[>]=], '>', _pcre_flags) 40 | str = ngx.re.gsub(str, [=["]=], '"', _pcre_flags) 41 | str = ngx.re.gsub(str, [=[']=], "'", _pcre_flags) 42 | str = ngx.re.gsub(str, [=[&#(\d+);]=], 43 | function(n) return string.char(n[1]) end, _pcre_flags) 44 | str = ngx.re.gsub(str, [=[&#x(\d+);]=], 45 | function(n) return string.char(tonumber(n[1],16)) end, _pcre_flags) 46 | str = ngx.re.gsub(str, [=[&]=], '&', _pcre_flags) 47 | return str 48 | end, 49 | cmd_line = function(value) 50 | local str = tostring(value) 51 | str = ngx.re.gsub(str, [=[[\\'"^]]=], '', _pcre_flags) 52 | str = ngx.re.gsub(str, [=[\s+/]=], '/', _pcre_flags) 53 | str = ngx.re.gsub(str, [=[\s+[(]]=], '(', _pcre_flags) 54 | str = ngx.re.gsub(str, [=[[,;]]=], ' ', _pcre_flags) 55 | str = ngx.re.gsub(str, [=[\s+]=], ' ', _pcre_flags) 56 | return string.lower(str) 57 | end, 58 | base64_encode = function(value) 59 | return ngx.encode_base64(value) 60 | end, 61 | base64_decode = function(value) 62 | print('b64decode value: ', value) 63 | local t_val = ngx.decode_base64(tostring(value)) 64 | print('b64decode value: ', t_val) 65 | return (t_val) or value 66 | end, 67 | lowercase = function(value) 68 | return string.lower(tostring(value)) 69 | end, 70 | normalise_path = function(value) 71 | while (ngx.re.match(value, [=[[^/][^/]*/\.\./|/\./|/{2,}]=], _pcre_flags)) do 72 | value = ngx.re.gsub(value, [=[[^/][^/]*/\.\./|/\./|/{2,}]=], '/', _pcre_flags) 73 | end 74 | return value 75 | end, 76 | normalise_path_win = function(value) 77 | value = string_gsub(value, [[\]], [[/]]) 78 | return _M.lookup['normalise_path'](value) 79 | end, 80 | remove_comments = function(value) 81 | return ngx.re.gsub(value, [=[\/\*(\*(?!\/)|[^\*])*\*\/]=], '', _pcre_flags) 82 | end, 83 | remove_comments_char = function(value) 84 | return ngx.re.gsub(value, [=[\/\*|\*\/|--|#]=], '', _pcre_flags) 85 | end, 86 | remove_whitespace = function(value) 87 | return ngx.re.gsub(value, [=[\s+]=], '', _pcre_flags) 88 | end, 89 | trim = function(value) 90 | return ngx.re.gsub(value, [=[^\s*|\s+$]=], '') 91 | end, 92 | trim_left = function(value) 93 | return ngx.re.sub(value, [=[^\s+]=], '') 94 | end, 95 | trim_right = function(value) 96 | return ngx.re.sub(value, [=[\s+$]=], '') 97 | end, 98 | uri_decode = function(value) 99 | return ngx.unescape_uri(value) 100 | end, 101 | sql_hex_decode = function(value) 102 | if (string.find(value, '0x', 1, true)) then 103 | value = string.sub(value, 3) 104 | return _M.hex_decode(value) 105 | else 106 | return value 107 | end 108 | end 109 | } 110 | 111 | 112 | return _M 113 | -------------------------------------------------------------------------------- /lua_scripts/module/utils.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | function _M.get_client_ip() 4 | local ip = ngx.var.remote_addr 5 | local xff = ngx.req.get_headers()['X-Forwarded-For'] 6 | if xff then 7 | local ips = {} 8 | for i in xff:gmatch(',') do 9 | ips[#ips+1] = i 10 | end 11 | if #ips >= 2 then 12 | ip = ips[#ips-1] 13 | elseif #ips == 1 then 14 | ip = ips[1] 15 | end 16 | end 17 | return ip 18 | end 19 | 20 | -- io 21 | function _M.over_write_file(file_name, data) 22 | local file = io.open(file_name, 'w+') 23 | if file then 24 | file:write(data) 25 | file:close() 26 | return true 27 | else 28 | ngx.log(ngx.ERR, 'over write file failed: ', file_name) 29 | end 30 | end 31 | 32 | function _M.read_small_file(file_name) 33 | local file, err = io.open(file_name) 34 | local data = {} 35 | if file then 36 | for line in file:lines() do 37 | data[#data+1] = line 38 | end 39 | file:close() 40 | return table.concat(data, '\n') 41 | else 42 | ngx.log(ngx.ERR, '---failed open file: '..file_name) 43 | end 44 | end 45 | 46 | function _M.check_did(did) 47 | if did then 48 | local bs = ngx.decode_base64(did) 49 | if not bs then 50 | return 51 | end 52 | local t = {} 53 | for token in string.gmatch(bs, '[^-]+') do 54 | t[#t+1] = token 55 | end 56 | --ngx.log(ngx.ERR, '---raw', table.concat(t, '-')) 57 | local key='\xf9\xfdQ"\xe7\x9fU\xc85\xa4{\xe0\x9d\x9d\x1b\xa5' 58 | local msg = t[2] .. t[1] 59 | local digest = ngx.encode_base64(ngx.hmac_sha1(key, msg)) 60 | if digest==t[3] then 61 | return true 62 | end 63 | end 64 | end 65 | 66 | function _M.table_values(t) 67 | local rt = {} 68 | if type(t) == 'table' and #t > 0 then 69 | for k, v in pairs(t) do 70 | rt[#rt+1] = v 71 | end 72 | return rt 73 | --else 74 | -- print('no table found') 75 | end 76 | end 77 | 78 | function _M.table_keys(t) 79 | local rt = {} 80 | if type(t) == 'table' and #t > 0 then 81 | for k, v in pairs(t) do 82 | rt[#rt+1] = k 83 | end 84 | return rt 85 | --else 86 | -- print('no table found') 87 | end 88 | end 89 | 90 | return _M 91 | -------------------------------------------------------------------------------- /lua_scripts/module/white_list.lua: -------------------------------------------------------------------------------- 1 | --local request_tester = require('request_tester') 2 | local rule = require('rule') 3 | 4 | local _M = {} 5 | 6 | local function filter(white_list) 7 | if white_list ~= nil and next(white_list) ~= nil then 8 | for _, rule in pairs(white_list) do 9 | local match = {} 10 | for key, value in pairs(rule) do 11 | if key == 'IP' then 12 | match['IP'] = { 13 | ['operator'] = 'EQL', 14 | ['value'] = value 15 | } 16 | elseif key == 'URI' then 17 | match['URI'] = { 18 | ['operator'] = 'REG', 19 | ['value'] = value 20 | } 21 | end 22 | end 23 | if rule.match({match}) == true then 24 | return true 25 | end 26 | end 27 | end 28 | return false 29 | end 30 | 31 | _M.filter = filter 32 | 33 | return _M 34 | -------------------------------------------------------------------------------- /lua_scripts/on_access.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local cfg = require('config') 3 | local configs = cfg.configs 4 | local PHASE_ACCESS = 'access' 5 | 6 | if not configs then 7 | print('waf configs', tostring(configs)) 8 | return 9 | end 10 | 11 | local host_cfg = nil 12 | local host = ngx.var.host 13 | if host then 14 | host_cfg = configs[host] 15 | if not host_cfg then 16 | print('host config', tostring(host_cfg)) 17 | return ngx.exit(ngx.HTTP_FORBIDDEN) 18 | end 19 | ngx.ctx.host_cfg = host_cfg 20 | else 21 | return ngx.exit(ngx.HTTP_FORBIDDEN) 22 | end 23 | 24 | local white_list = require('white_list') 25 | local cc = require('cc') 26 | local filter = require('filter') 27 | 28 | if host_cfg.waf_mode == cfg.WAF_OFF then -- waf关闭,跳过,否则执行过滤 29 | print('----waf mode off') 30 | elseif white_list.filter(host_cfg.white_list) then 31 | print('------come to white_list') 32 | elseif host_cfg.cc_mode >= cfg.WAF_LOG and cc.filter( 33 | host, host_cfg) then 34 | print('------come to cc_deny') 35 | elseif filter.filter(host_cfg, PHASE_ACCESS) then 36 | print('------come to filter') 37 | else 38 | --print('------go ahead') 39 | return 40 | end 41 | -------------------------------------------------------------------------------- /lua_scripts/on_body_filter.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local filter = require('filter') 3 | local cjson = require('cjson') 4 | local gzip = require('gzipd') 5 | local PHASE_BODY = 'body' 6 | local GZIP = 'gzip' 7 | -- keep in mind, this module may be called multiple times in a request 8 | 9 | local ctx = ngx.ctx 10 | local host_cfg = ctx.host_cfg 11 | local content_type = ngx.header.content_type 12 | print('Content-Type: ', content_type) 13 | 14 | 15 | local function _decompress(response_body) 16 | local content_encoding = ngx.header["Content-Encoding"] 17 | if content_encoding == GZIP then 18 | local success, data = pcall(gzip.decompress, gzip_data) 19 | if success then 20 | return data 21 | end 22 | end 23 | return response_body 24 | end 25 | 26 | local function _access_body(host_cfg) 27 | return host_cfg and host_cfg['response_body_access'] and host_cfg['response_body_mime_type']:find(content_type) and host_cfg['body_filters'] 28 | end 29 | 30 | -- filter only when configed to access body 31 | if _access_body(host_cfg) then 32 | print('start filter response body') 33 | local chunk, eof = ngx.arg[1], ngx.arg[2] 34 | local buf = ngx.ctx.response_body 35 | if eof then 36 | if buf then 37 | -- filter all buffered output, this should be execute once 38 | ngx.ctx.response_body = _decompress(buf .. chunk) 39 | print('=======', ngx.ctx.response_body) 40 | filter.filter(host_cfg, PHASE_BODY) 41 | ngx.arg[1] = ngx.ctx.response_body 42 | return 43 | end 44 | return 45 | end 46 | if buf then 47 | ngx.ctx.response_body = buf .. chunk 48 | else 49 | ngx.ctx.response_body = chunk 50 | end 51 | 52 | ngx.arg[1] = nil 53 | end 54 | -------------------------------------------------------------------------------- /lua_scripts/on_header_filter.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local filter = require('filter') 3 | local PHASE_HEADER = 'header' 4 | _M = {} 5 | local ctx = ngx.ctx 6 | local host_cfg = ctx.host_cfg 7 | local content_type = ngx.header["content-type"] 8 | 9 | local function _modify_body(host_cfg) 10 | return host_cfg and host_cfg['response_body_access'] and content_type and host_cfg['response_body_mime_type']:find(content_type) and host_cfg['body_filters'] and ngx.header.content_length 11 | end 12 | 13 | --check body maybe modified 14 | if _modify_body(host_cfg) then 15 | print('delete content-length in response header') 16 | ngx.header.content_length = nil 17 | end 18 | 19 | local function _header_filte(host_cfg) 20 | return host_cfg and host_cfg['header_filters'] 21 | end 22 | 23 | if _header_filte(host_cfg) then 24 | print('header filtering') 25 | filter.filter(host_cfg, PHASE_HEADER) 26 | else 27 | print('no header filters config') 28 | end 29 | -------------------------------------------------------------------------------- /lua_scripts/on_init.lua: -------------------------------------------------------------------------------- 1 | local config = require 'config' 2 | config.init() 3 | -------------------------------------------------------------------------------- /lua_scripts/on_log.lua: -------------------------------------------------------------------------------- 1 | local log = require('log') 2 | log.do_log() 3 | -------------------------------------------------------------------------------- /lua_scripts/on_worker_init.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | local config = require('config') 3 | if config.log_type == "kafka" then 4 | local kafka = require('kafka') 5 | kafka.init() 6 | end 7 | -------------------------------------------------------------------------------- /sample_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "13": { 4 | "match": [ 5 | { 6 | "transforms": [], 7 | "operator": "REG", 8 | "value": "Xmhl", 9 | "items": [ 10 | "RESPONSE_BODY" 11 | ] 12 | } 13 | ], 14 | "attack_tag": "BODY_LIMIT", 15 | "id": "13", 16 | "risk_level": 2, 17 | "desc": "Response Body filter" 18 | }, 19 | "12": { 20 | "match": [ 21 | { 22 | "transforms": [], 23 | "operator": "gt", 24 | "value": "64000", 25 | "items": [ 26 | "ALL_ARGS_COMBINED_SIZE" 27 | ] 28 | } 29 | ], 30 | "attack_tag": "SIZE_LIMIT", 31 | "id": "12", 32 | "risk_level": 2, 33 | "desc": "Total Argument too long, More than 64000" 34 | }, 35 | "11": { 36 | "match": [ 37 | { 38 | "transforms": [ 39 | "count" 40 | ], 41 | "operator": "gt", 42 | "value": "100", 43 | "items": [ 44 | "ALL_ARGS" 45 | ] 46 | } 47 | ], 48 | "attack_tag": "SIZE_LIMIT", 49 | "id": "11", 50 | "risk_level": 2, 51 | "desc": "Argument value too long, More than 100" 52 | }, 53 | "10": { 54 | "match": [ 55 | { 56 | "transforms": [ 57 | "length" 58 | ], 59 | "operator": "gt", 60 | "value": "100", 61 | "items": [ 62 | "ALL_ARGS_NAMES" 63 | ] 64 | } 65 | ], 66 | "attack_tag": "SIZE_LIMIT", 67 | "id": "10", 68 | "risk_level": 2, 69 | "desc": "Argument name too long, More than 100" 70 | }, 71 | "9": { 72 | "match": [ 73 | { 74 | "transforms": [ 75 | "count" 76 | ], 77 | "operator": "gt", 78 | "value": "100", 79 | "items": [ 80 | "ALL_ARGS_NAMES" 81 | ] 82 | } 83 | ], 84 | "attack_tag": "SIZE_LIMIT", 85 | "id": "9", 86 | "risk_level": 2, 87 | "desc": "Too many arguments in request, More than 100" 88 | }, 89 | "8": { 90 | "match": [ 91 | { 92 | "transforms": [ 93 | "lowercase" 94 | ], 95 | "operator": "~", 96 | "value": "W1xuXHJdKD86Y29udGVudC0odHlwZXxsZW5ndGgpfHNldC1jb29raWV8bG9jYXRpb24pOg==", 97 | "items": [ 98 | "ALL_ARGS", 99 | "ALL_ARGS_NAMES", 100 | "COOKIES", 101 | "COOKIES_NAMES" 102 | ] 103 | } 104 | ], 105 | "attack_tag": "RESPONSE-SPLITING", 106 | "id": "8", 107 | "risk_level": 1, 108 | "desc": "HTTP Response Splitting Attack - 1" 109 | }, 110 | "7": { 111 | "match": [ 112 | { 113 | "transforms": [ 114 | "html_decode", 115 | "lowercase" 116 | ], 117 | "operator": "~", 118 | "value": "KD86XCgoPzpcVyo/KD86b2JqZWN0Yyg/OmF0ZWdvcnl8bGFzcyl8aG9tZWRpcmVjdG9yeXxbZ3VdaWRudW1iZXJ8Y24pXGJcVyo/PXxbXlx3XHg4MC1ceEZGXSo/W1whXCZcfF1bXlx3XHg4MC1ceEZGXSo/XCgpfFwpW15cd1x4ODAtXHhGRl0qP1woW15cd1x4ODAtXHhGRl0qP1tcIVwmXHxdKQ==", 119 | "items": [ 120 | "ALL_ARGS", 121 | "ALL_ARGS_NAMES", 122 | "COOKIES", 123 | "COOKIES_NAMES" 124 | ] 125 | } 126 | ], 127 | "attack_tag": "LDAP-Injection", 128 | "id": "7", 129 | "risk_level": 1, 130 | "desc": "LDAP Injection Attack" 131 | }, 132 | "6": { 133 | "match": [ 134 | { 135 | "transforms": [ 136 | "uri_decode" 137 | ], 138 | "operator": "~", 139 | "value": "QVJHUw==", 140 | "items": [ 141 | "ALL_ARGS" 142 | ] 143 | } 144 | ], 145 | "attack_tag": "Anomaly-Request", 146 | "id": "6", 147 | "risk_level": 1, 148 | "desc": "Meta-Character Anomaly Detection Alert - Repetative Non-Word Characters" 149 | }, 150 | "5": { 151 | "match": [ 152 | { 153 | "transforms": [ 154 | "uri_decode", 155 | "normalize_path" 156 | ], 157 | "operator": "CTN", 158 | "value": "a", 159 | "items": [ 160 | "ALL_ARGS", 161 | "ALL_ARGS_NAMES", 162 | "COOKIES", 163 | "COOKIES_NAMES" 164 | ] 165 | } 166 | ], 167 | "attack_tag": "COMMAND_INJECTION,WASCTC/WASC-31,OWASP_TOP_10/A1,PCI/6.5.2", 168 | "id": "5", 169 | "risk_level": 3, 170 | "desc": "System Command Injection - such as curl, wget and cc" 171 | }, 172 | "2": { 173 | "match": [ 174 | { 175 | "transforms": [ 176 | "normalize_path" 177 | ], 178 | "operator": "ends_with", 179 | "value": [ 180 | ".asa", 181 | ".asax", 182 | ".ascx" 183 | ], 184 | "items": [ 185 | "URI" 186 | ] 187 | } 188 | ], 189 | "attack_tag": "restricted_extensions", 190 | "id": "1", 191 | "risk_level": 2, 192 | "desc": "不允许的文件后缀" 193 | }, 194 | "1": { 195 | "match": [ 196 | { 197 | "transforms": [], 198 | "operator": "!contain", 199 | "value": "GET,HEAD,POST,OPTIONS,PUT", 200 | "items": [ 201 | "METHOD" 202 | ] 203 | } 204 | ], 205 | "attack_tag": "FORBIDEN_METHOD", 206 | "id": "1", 207 | "risk_level": 2, 208 | "desc": "请求了不允许的方法" 209 | } 210 | }, 211 | "datas": [ 212 | { 213 | "data": { 214 | "header_filters": [], 215 | "body_filters": [ 216 | { 217 | "mode": 1, 218 | "action_vars": { 219 | "pattern": "he", 220 | "new": "ha" 221 | }, 222 | "action": "GSUB", 223 | "rule_id": "13" 224 | } 225 | ], 226 | "filters": [ 227 | { 228 | "mode": 1, 229 | "action_vars": { 230 | "redirect_url": "http://your_redirect_url" 231 | }, 232 | "action": "REDIRECT", 233 | "rule_id": "5" 234 | }, 235 | { 236 | "mode": 1, 237 | "action_vars": { 238 | "redirect_url": "http://your_redirect_url" 239 | }, 240 | "action": "REDIRECT", 241 | "rule_id": "6" 242 | } 243 | ], 244 | "response_body_limit": 524288, 245 | "response_body_mime_type": "text/plain,text/html,text/xml", 246 | "response_body_access": 1, 247 | "request_body_nofile_limit": 131072, 248 | "request_body_limit": 131072, 249 | "request_body_access": 1, 250 | "upload_file_access": 1, 251 | "waf_mode": 1, 252 | "black_mode": -1, 253 | "cc_mode": 1 254 | }, 255 | "nid": "20d92060838a367fbc0fc072935f4f97", 256 | "site_name": "localhost" 257 | } 258 | ], 259 | "global_config": { 260 | }, 261 | "version": "4.0", 262 | "hash": "", 263 | "code": 200, 264 | "message": "" 265 | } 266 | --------------------------------------------------------------------------------