local redis_util = require "redis_util" local action = require "action" local cc = require "cc" local fileUtils = require "file" local ck = require "resty.cookie" local geo = require "geoip" local libinjection = require "resty.libinjection" local config = require "config" local cjson = require "cjson" local utils = require "utils" local pairs = pairs local ipairs = ipairs local tostring = tostring local type = type local next = next local tonumber = tonumber local concat_table = table.concat local ngx_re_find = ngx.re.find local decode = cjson.decode local ngx_re_gmatch = ngx.re.gmatch local ngx_re_match = ngx.re.match local exec_action = action.exec_action local _M = {} local function is_global_state_on(name) return config.is_global_state_on(name) end local function is_site_state_on(name) local site_config = config.get_site_config(ngx.ctx.website_key) if site_config ~= nil then return site_config[name]["state"] == "on" and true or false end return true end local function is_state_on(name) return is_site_state_on(name) and is_global_state_on(name) end local function get_site_config(name) local site_config = config.get_site_config(ngx.ctx.website_key) if site_config ~= nil then return site_config[name] end return config.get_global_config(name) end local function get_site_rule(name) local site_rules = config.get_site_rules(ngx.ctx.website_key) if site_rules ~= nil then return site_rules[name] end return config.get_global_rules(name) end local function get_global_rules(name) return config.get_global_rules(name) end local function get_global_config(name) return config.get_global_config(name) end local function is_rule_state_on(rule_table) return rule_table["state"] == "on" and true or false end local function matches(input, regex, ctx, nth) if not nth then nth = 0 end return ngx_re_find(input, regex, "isjo", ctx, nth) end local function match_rule(rule_table, str) if str == nil or next(rule_table) == nil then return false end for _, t in ipairs(rule_table) do if matches(str, t.rule) then return true, t end end return false end local function match_ip(ip_rule, ip, ipn) if ip_rule == nil or ip == nil then return false end if is_rule_state_on(ip_rule) == false then return false end local ip_rule_type = ip_rule.type if utils.is_ipv6(ip) and ip_rule_type == "ipv6" then if ip == ip_rule.ipv6 then return true end return false end if ip_rule.type == "ipv4" then if ipn == tonumber(ip_rule.ipv4) then return true end elseif ip_rule.type == "ipArr" then local ipArr = ip_rule.ipArr if utils.is_ip_in_array(ipn, ipArr.start, ipArr["end"]) then return true end elseif ip_rule.type == "ipGroup" then --TODO 匹配 IP 组 end return false end local function xss_and_sql_check(body) if body then if is_global_state_on("xss") or is_global_state_on("sql") then for k, v in pairs(body) do if type(v) == 'string' then if is_site_state_on("xss") then local is_xss, fingerprint = libinjection.xss(tostring(v)) local xss_config = get_site_config("xss") if is_xss then exec_action(xss_config, { rule = tostring(k) .. '=' .. tostring(v) }) return end end if is_site_state_on("sql") then local is_sqli, fingerprint = libinjection.sqli(tostring(v)) local sql_config = get_site_config("sql") if is_sqli then exec_action(sql_config, { rule = tostring(k) .. '=' .. tostring(v) }) return end end end end end end end local function get_request_body() ngx.req.read_body() local body_data = ngx.req.get_body_data() if not body_data then local body_file = ngx.req.get_body_file() if body_file then body_data = fileUtils.read_file2string(body_file, true) end end return body_data end function _M.is_white_ip() if is_global_state_on("ipWhite") then local ip = ngx.ctx.ip if ip == "unknown" then return false end if ip == "127.0.0.1" then return true end local ipn = utils.ipv4_to_int(ip) local ip_rules = get_global_rules("ipWhite") for _, ip_rule in pairs(ip_rules) do if match_ip(ip_rule, ip, ipn) then return true end end end return false end function _M.allow_location_check() if is_state_on("geoRestrict") then local geo_ip = ngx.ctx.geoip if geo_ip and geo_ip.iso and geo_ip.iso ~= "" then local iso = geo_ip.iso local geo_config = get_site_config("geoRestrict") local exist = false for _, rule in ipairs(geo_config.rules) do if iso == rule then exist = true break end end local default_geo_config = { action = "deny", code = 444, type = "geoRestrict", state = "on", rule = iso } if exist then if geo_config.action == "allow" then return true end if geo_config.action == "deny" then exec_action(default_geo_config, default_geo_config) return false end else if geo_config.action == "allow" then exec_action(default_geo_config, default_geo_config) return false end end end end end function _M.default_ip_black() if is_state_on("defaultIpBlack") then if geo.is_default_black_ip(ngx.ctx.ip) then exec_action(get_site_config("defaultIpBlack"), { rule = ngx.ctx.ip }) end end end function _M.black_ip() if is_global_state_on("ipBlack") then local ip = ngx.ctx.ip if ip == "unknown" then return false end local exists = nil if config.is_redis_on() then exists = redis_util.get("black_ip:" .. ip) else exists = ngx.shared.waf_black_ip:get(ip) end if not exists then local ip_black_list = get_global_rules("ipBlack") local ipn = utils.ipv4_to_int(ip) for _, ip_rule in pairs(ip_black_list) do if match_ip(ip_rule, ip, ipn) then exists = true break end end end if exists then exec_action(get_global_config("ipBlack")) end return exists end return false end function _M.method_check() local method = ngx.req.get_method() local method_white_list = get_site_rule("methodWhite") for _, method_rule in ipairs(method_white_list) do if method_rule.rule == method and method_rule.state == 'off' then local method_config = get_global_config("methodWhite") exec_action(method_config, method_rule) return false end end return true end function _M.bot_check() if is_state_on("bot") then local ruri = ngx.var.request_uri local uri = ngx.var.uri local bot_rule = get_site_config("bot") if uri == bot_rule.uri or ruri == bot_rule.uri then exec_action(bot_rule) end end end function _M.black_ua() if is_global_state_on("uaBlack") then if type(ngx.ctx.ua) ~= 'string' then ngx.exit(200) end local m, mr = match_rule(get_global_rules("uaBlack"), ngx.ctx.ua) if m then exec_action(get_global_config("uaBlack"), mr) end end end function _M.default_ua_black() if is_state_on("defaultUaBlack") then if type(ngx.ctx.ua) ~= 'string' then ngx.exit(200) end local m, mr = match_rule(get_global_rules('defaultUaBlack'), ngx.ctx.ua) if m then exec_action(get_global_config('defaultUaBlack'), mr) end end end function _M.is_white_ua() if is_global_state_on("uaWhite") then local ua = utils.get_header("user-agent") if not ua then return false end if type(ua) ~= 'string' then ngx.exit(200) end for _, wa in ipairs(get_global_rules("uaWhite")) do if ngx.ctx.ua == wa then return true end end end return false end function _M.cc() if is_state_on("cc") then if cc.check_access_token() then return end local ip = ngx.ctx.ip local cc_config = get_site_config("cc") local key = ip if config.is_redis_on() then key = "cc_req_count:" .. key local count, _ = redis_util.incr(key, cc_config.duration) if not count then redis_util.set(key, 1, cc_config.duration) elseif count > cc_config.threshold then exec_action(cc_config, { rule = cc_config.rule }) return end else local limit = ngx.shared.waf_limit local count, _ = limit:incr(key, 1, 0, cc_config.duration) if not count then limit:set(key, 1, cc_config.duration) elseif count > cc_config.threshold then exec_action(cc_config, { rule = cc_config.rule }) return end end end end function _M.cc_url() if is_state_on("ccurl") then local ip = ngx.ctx.ip local key = ip local urlcc_rules = get_site_rule("ccurl") local urlcc_config = get_site_config("ccurl") local uri = ngx.var.uri local m, mr = match_rule(urlcc_rules, uri) if not m or not mr then return end key = uri .. key if config.is_redis_on() then key = "url_cc_req_count:" .. key local count, _ = redis_util.incr(key, mr.duration) if not count then redis_util.set(key, 1, mr.duration) elseif count > mr.threshold then exec_action(urlcc_config, { rule = mr.rule }) return end else local limit = ngx.shared.waf_limit local count, _ = limit:incr(key, 1, 0, mr.duration) if not count then limit:set(key, 1, urlcc_config.duration) elseif count > mr.threshold then exec_action(urlcc_config, { rule = mr.rule }) return end end end end function _M.is_white_url() if is_global_state_on("urlWhite") then local url = ngx.var.uri if url == nil or url == " " then return false end local m, _ = match_rule(get_global_rules("urlWhite"), url) if m then return true end return false end return false end function _M.black_url() if is_global_state_on("urlBlack") then local url = ngx.var.uri if url == nil or url == "" then return false end local m, mr = match_rule(get_global_rules("urlBlack"), url) if m then exec_action(get_global_config("urlBlack"), mr) return end end end function _M.args_check() if is_state_on("args") then local args = ngx.req.get_uri_args() if args then local args_list = get_global_rules("args") for _, val in pairs(args) do local val_arr = val if type(val) == "table" then val_arr = concat_table(val, ", ") end if val_arr and type(val_arr) ~= "boolean" and val_arr ~= "" then local m, mr = match_rule(args_list, utils.unescape_uri(val_arr)) if m then exec_action(get_global_config("args"), mr) return end end end xss_and_sql_check(args) end end end function _M.cookie_check() local cookie = ngx.var.http_cookie if cookie and is_state_on("cookie") then local cookieList = get_site_rule('cookie') local m, mr = match_rule(cookieList, cookie) if m then local rule_config = get_global_config('cookie') exec_action(rule_config, mr) return true end end return false end function _M.header_check() if is_state_on("header") then local headers_rule = get_site_rule("header") local headers_config = get_site_config("header") local referer = ngx.var.http_referer if referer and referer ~= "" then local m = match_rule(headers_rule, referer) if m then exec_action(headers_config) end end local headers = utils.get_headers() if headers then for k, v in pairs(headers) do local m1, mr1 = match_rule(headers_rule, k) if m1 then exec_action(headers_config, mr1) end local m2, mr2 = match_rule(headers_rule, v) if m2 then exec_action(headers_config, mr2) end end end end end function _M.post_check() local content_type = ngx.ctx.content_type local content_length = ngx.ctx.content_length if ngx.ctx.method == "GET" or not content_type or type(content_type) ~= 'string' then return end if content_length == nil or content_length == 0 then return end if ngx_re_find(content_type, '^application/json', "ijo") then local data = get_request_body() if data then xss_and_sql_check(decode(data)) end end if is_site_state_on('fileExtCheck') and ngx_re_find(content_type, [[multipart]], 'ijo') then if not ngx_re_match(content_type, '^multipart/form-data; boundary=') then return end local boundary_value = ngx_re_match(content_type, '^multipart/form-data; boundary=(.+)') if boundary_value == nil then return end local data = get_request_body() if data == nil then return end local iterator = ngx_re_gmatch(data, [[Content-Disposition.+filename=.+]], 'ijo') if not iterator then return end local rule = get_site_config("fileExtCheck") while true do local m = iterator() if m then local match = ngx_re_match(m[0], 'Content-Disposition: form-data; (.+)filename="(.+)\\.(.*)"', 'ijo') if match then local extension = match[3] for _, ext in ipairs(rule.extList) do if extension == ext then exec_action(rule) end end end else break end end end end function _M.acl() local rules = get_site_rule("acl") for _, rule in pairs(rules) do if rule.state == nil or rule.state == "off" then goto continue end local conditions = rule.conditions local match = true for _, condition in pairs(conditions) do local field = condition.field local field_name = condition.name local pattern = condition.pattern local match_value = '' if field == 'URL' then match_value = ngx.var.request_uri elseif field == 'Cookie' then if field_name ~= nil and field_name ~= '' then local cookies, _ = ck:new() if not cookies then match = false break else match_value, _ = cookies:get(field_name) end else match_value = ngx.var.http_cookie end elseif field == 'Header' then local headers = ngx.req.get_headers() if headers then if field_name ~= nil and field_name ~= '' then match_value = headers[field_name] else match_value = concat_table(headers, '') end else match = false break end elseif field == 'Referer' then match_value = ngx.var.http_referer elseif field == 'User-Agent' then match_value = ngx.var.http_user_agent elseif field == 'IP' then match_value = ngx.ctx.ip end if pattern == '' then if match_value ~= nil and match_value ~= '' then match = false break end else if not matches(match_value, pattern) then match = false break end end end if match then rule.type = "acl" exec_action(rule) end :: continue :: end end return _M