diff --git a/src/plugins/third_party/auth-plugin.lua b/src/plugins/third_party/auth-plugin.lua new file mode 100644 index 0000000..3fd305c --- /dev/null +++ b/src/plugins/third_party/auth-plugin.lua @@ -0,0 +1,150 @@ +--- +--- 作者: MCQSJ(https://github.com/MCQSJ) +--- 更新日期: 2024/12/21 +--- + +local ngx = ngx +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_print = ngx.print +local ngx_exit = ngx.exit +local ngx_today = ngx.today +local ngx_kv = ngx.shared + +local _M = { + version = 0.1, + name = "auth-plugin" -- 插件名称 +} + +-- 配置 +local valid_domains = { + "test.com", -- 需要保护的域名列表 + "test1.cn" +} + +local valid_username = "admin" +local valid_password = "password123" -- 强密码建议修改 +local session_duration = 7200 -- 2小时,以秒为单位 +local max_login_attempts = 5 -- 最大登录失败次数 + +local function escape_html(str) + if not str then return "" end + local replacements = { + ["&"] = "&", + ["<"] = "<", + [">"] = ">", + ['"'] = """, + ["'"] = "'", + } + return (str:gsub("[&<>'\"]", function(c) return replacements[c] end)) +end + +local function get_login_page(req_uri, error_message) + local escaped_error_message = escape_html(error_message or "") + local form_action = escape_html(req_uri or "/") + + return [[ + + + + + + 身份验证 + + + +
+

身份验证

+ ]] .. (escaped_error_message ~= "" and '

' .. escaped_error_message .. '

' or "") .. [[ +
+ + + + + +
+

您的访问受保护,请输入正确的账号密码。

+
+ + + ]] +end + +local function validate_login(waf) + local form = waf.form["FORM"] + if form then + local username = form["username"] + local password = form["password"] + if username == valid_username and password == valid_password then + return true + end + end + return false +end + +function _M.req_post_filter(waf) + local host = waf.host + local req_uri = waf.reqUri + local method = waf.method + + local is_protected = false + for _, domain in ipairs(valid_domains) do + if string.lower(host) == string.lower(domain) then + is_protected = true + break + end + end + + if not is_protected then + return + end + + local login_attempts_key = "login_attempts:" .. waf.ip .. ":" .. host + local login_attempts = ngx_kv.ipCache and ngx_kv.ipCache:get(login_attempts_key) or 0 + + if login_attempts >= max_login_attempts then + ngx_kv.ipBlock:incr(waf.ip, 1, 0) + waf.msg = "IP因登录失败次数过多已被拦截" + waf.rule_id = 10001 + waf.deny = true + return ngx_exit(403) + end + + local session_key = "auth:" .. waf.ip .. ":" .. host + local is_authenticated = ngx_kv.ipCache and ngx_kv.ipCache:get(session_key) + + if not is_authenticated then + if method == "POST" then + if validate_login(waf) then + ngx_kv.ipCache:set(session_key, true, session_duration) + ngx_kv.ipCache:delete(login_attempts_key) + return + else + login_attempts = login_attempts + 1 + ngx_kv.ipCache:set(login_attempts_key, login_attempts, 3600) + + local error_message = "用户名或密码错误,请重试。" + ngx.header.content_type = "text/html; charset=utf-8" + return ngx.print(get_login_page(req_uri, error_message)) + end + else + ngx.header.content_type = "text/html; charset=utf-8" + return ngx.print(get_login_page(req_uri, nil)) + end + end + +end + +return _M diff --git a/src/rules/third_party/brute-force-login-prevention.lua b/src/rules/third_party/brute-force-login-prevention.lua new file mode 100644 index 0000000..770f1ba --- /dev/null +++ b/src/rules/third_party/brute-force-login-prevention.lua @@ -0,0 +1,40 @@ +--[[ +规则名称: 登录爆破防护 +过滤阶段: 请求阶段 +危险等级: 高危 +规则描述: 针对路径中包含登录、注册等关键词的URL进行防护 +作者: MCQSJ(https://github.com/MCQSJ) +更新日期: 2024/12/21 +--]] + +-- 配置参数 +local threshold = 30 -- 错误次数阈值 +local timeWindow = 180 -- 时间窗口,单位为秒 +local banDuration = 1440 * 60 -- 封禁时间,单位为秒 + +local sh = waf.ipCache +local bruteForceKey = 'brute-force-login:' .. waf.ip + +-- 定义特征路径关键词列表 +local targetPaths = { "login", "signin", "signup", "register", "reset", "passwd", "account", "user" } + +if not waf.pmMatch(waf.toLower(waf.uri), targetPaths) then + return false +end + +local requestCount, flag = sh:get(bruteForceKey) +if not requestCount then + sh:set(bruteForceKey, 1, timeWindow, 1) +else + if flag == 2 then + return waf.block(true) + end + + sh:incr(bruteForceKey, 1) + if requestCount + 1 > threshold then + sh:set(bruteForceKey, requestCount + 1, banDuration, 2) + return true, "检测到登录接口发生爆破攻击,已封禁IP", true + end +end + +return false diff --git a/src/rules/third_party/frequent-block-detection.lua b/src/rules/third_party/frequent-block-detection.lua new file mode 100644 index 0000000..a3a7007 --- /dev/null +++ b/src/rules/third_party/frequent-block-detection.lua @@ -0,0 +1,31 @@ +--[[ +规则名称: 高频攻击防护 +过滤阶段: 请求阶段 +危险等级: 高危 +规则描述: 针对发起高频率攻击的行为进行防护 +作者: MCQSJ(https://github.com/MCQSJ) +更新日期: 2024/12/21 +!!!注意: 因为南墙WAF特性,此规则生效对规则ID有要求,需要将此规则与南墙自带规则的第一个规则交换位置才能生效!!! +]] + +-- 配置参数 +local threshold = 60 -- 错误次数阈值 +local banDuration = 1440 * 60 -- 封禁时间,单位为秒 + +local sh = waf.ipCache +local ip_stats = waf.ipBlock +local ip = waf.ip +local block_key = "blocked-" .. ip + +local c, f = sh:get(block_key) +if c and f == 2 then + return waf.block(true) +end + +local recent_count = ip_stats:get(ip) +if recent_count and recent_count > threshold then + sh:set(block_key, 1, banDuration, 2) + return true, "IP频繁触发拦截,已被拉黑", true +end + +return false diff --git a/src/rules/third_party/high-frequency-error-protection.lua b/src/rules/third_party/high-frequency-error-protection.lua new file mode 100644 index 0000000..b0a162f --- /dev/null +++ b/src/rules/third_party/high-frequency-error-protection.lua @@ -0,0 +1,47 @@ +--[[ +规则名称: 高频错误防护 +过滤阶段: 返回HTTP头阶段 +危险等级: 中危 +规则描述: 针对频繁触发错误的请求的行为进行防护 +作者: MCQSJ(https://github.com/MCQSJ) +更新日期: 2024/12/21 +--]] + +local function isSpecifiedError(status) + local allowed_errors = {400, 401, 403, 404, 405, 429, 444} + return waf.inArray(status, allowed_errors) +end + +-- 配置参数 +local threshold = 10 -- 错误次数阈值 +local timeWindow = 60 -- 时间窗口,单位为秒 +local banDuration = 1440 * 60 -- 封禁时间,1440分钟 = 86400秒 + +local ip = waf.ip + +local status = waf.status + +if not isSpecifiedError(status) then + return false +end + +local errorCache = waf.ipCache +local errorKey = "error:" .. ip + +local errorCount, flag = errorCache:get(errorKey) + +if not errorCount then + errorCache:set(errorKey, 1, timeWindow) +else + if flag == 2 then + return waf.block(true) + end + + errorCache:incr(errorKey, 1) + if errorCount + 1 >= threshold then + errorCache:set(errorKey, errorCount + 1, banDuration, 2) + return true, "高频错误触发,IP已被封禁", true + end +end + +return false