From 1f3bedec7fe047b6c502e3ddfcfa9224f1469b2d Mon Sep 17 00:00:00 2001 From: MCQSJ <147456216+MCQSJ@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:54:26 +0000 Subject: [PATCH] remove: third-party rules and update plugins - Remove 4 third-party rules from rules/third_party/ - Update auth-plugin to v3.4 (cookie auth, auto-renew, IP/header whitelist, login ban) - Add cc-protection v1.8 (origin circuit breaker, global rate limit, path rate limit, site rate limit) --- plugins/third_party/auth-plugin.lua | 536 ++++++++++-------- plugins/third_party/cc-protection.lua | 246 ++++++++ .../brute-force-login-prevention.lua | 40 -- .../dynamic-rate-limiting-protection.lua | 140 ----- .../third_party/frequent-block-detection.lua | 31 - .../high-frequency-error-protection.lua | 47 -- 6 files changed, 558 insertions(+), 482 deletions(-) create mode 100644 plugins/third_party/cc-protection.lua delete mode 100644 rules/third_party/brute-force-login-prevention.lua delete mode 100644 rules/third_party/dynamic-rate-limiting-protection.lua delete mode 100644 rules/third_party/frequent-block-detection.lua delete mode 100644 rules/third_party/high-frequency-error-protection.lua diff --git a/plugins/third_party/auth-plugin.lua b/plugins/third_party/auth-plugin.lua index e5625ba..23df9c2 100644 --- a/plugins/third_party/auth-plugin.lua +++ b/plugins/third_party/auth-plugin.lua @@ -1,8 +1,9 @@ --- ---- 作者: MCQSJ(https://github.com/MCQSJ) ---- 更新日期: 2025/07/24 ---- 身份验证插件,用于保护指定域名的请求,需要输入正确的用户名和密码才能访问。 ---- 更新内容:更新为cookie鉴权,避免ip变动导致频繁登录;支持自动续期,避免频繁登录;白名单路径更新为路径匹配,包含该路径均放行。 +--- 站点认证插件 +--- 为指定站点添加登录验证页面 +--- +--- 作者: YourName +--- 更新日期: 2026/05/31 --- local ngx = ngx local ngx_log = ngx.log @@ -11,44 +12,77 @@ local ngx_print = ngx.print local ngx_exit = ngx.exit local ngx_time = ngx.time local ngx_kv = ngx.shared -local resty_random = require "resty.random" -local resty_string = require "resty.string" +local resty_random = require("resty.random") +local resty_string = require("resty.string") +local ipmatcher = require("resty.ipmatcher") local _M = { - version = 2.1, - name = "auth-plugin" + version = 3.4, + name = "site-auth-plugin", + priority = 50 } --- 站点认证配置 {域名, 用户名, 密码} +-- <--- 配置参数 ---> + +-- 站点认证配置:{域名, 用户名, 密码, cookie有效期(秒)} +-- cookie有效期设为0表示会话cookie(浏览器关闭即失效),设为正数表示持久化时长 local site_auth_config = { - {"abc.com", "admin", "passwd"}, - {"123.cn", "admin", "passwd"} + -- {"example.com", "admin", "change-this-password", 0}, + -- {"private.example.com", "admin", "change-this-password", 86400}, } --- API白名单(路径前缀,包含此路径的请求均会直接放行) +-- IP白名单(CIDR格式):白名单内的IP无需认证即可访问 +local ip_whitelist = { + -- "192.168.1.0/24", + -- "10.0.0.0/8", +} + +-- API路径白名单:格式 "域名/路径前缀",匹配的路径无需认证(如图片直链) local api_whitelist = { - "abc.com/v1/", - "123.cn/i/" + -- "example.com/api/public/", + -- "static.example.com/assets/", } --- 全局设置 -local session_duration = 7200 -- 会话有效期(秒) -local cookie_name = "WAF_AUTH_SESSION" -local session_prefix = "sess:" -local max_login_attempts = 5 -- 最大登录失败次数 -local renew_threshold = 0.3 -- 续期阈值(剩余时间小于30%时续期) +-- 请求头放行白名单:匹配指定域名、请求头和值后无需认证 +-- host 支持具体域名或 "*";header 为请求头名称;value 为请求头值;case_sensitive 控制值是否区分大小写 +local header_allowlist = { + -- { host = "example.com", header = "X-Auth-Token", value = "change-me", case_sensitive = true }, + -- { host = "*", header = "X-Internal-Bypass", value = "change-me", case_sensitive = true }, +} + +-- 全局认证设置 +local default_session_duration = 7200 -- 会话cookie模式下的默认有效期,单位秒(默认2小时) +local cookie_name = "WAF_AUTH_SESSION" -- 认证会话Cookie名称 +local csrf_cookie_name = "WAF_AUTH_CSRF" -- 历史CSRF Cookie名称,仅用于清理旧Cookie +local session_prefix = "sess:" -- 会话存储键前缀 +local max_login_attempts = 5 -- 最大登录尝试次数(超过后封禁IP) +local login_attempt_window = 3600 -- 登录失败次数统计窗口,单位秒(默认1小时) +local login_ban_duration = 600 -- 登录失败封禁时长,单位秒(默认10分钟) +local renew_threshold = 0.3 -- 会话续期阈值(剩余有效期比例,0.3即剩余30%时续期) + +-- <--- 初始化 ---> + +local ipm, ipm_err +do + if #ip_whitelist > 0 then + ipm, ipm_err = ipmatcher.new(ip_whitelist) + if not ipm then + ngx_log(ngx_ERR, "auth-plugin: 初始化IP白名单失败: ", ipm_err) + end + end +end + +-- <--- 工具函数 ---> --- 生成安全的随机会话ID -local function generate_session_id() - local random_bytes = resty_random.bytes(32) +local function generate_random_token(length) + local random_bytes = resty_random.bytes(length) if not random_bytes then - ngx_log(ngx_ERR, "无法生成随机会话ID") + ngx_log(ngx_ERR, "auth-plugin: 无法生成随机token") return nil end return resty_string.to_hex(random_bytes) end --- 处理特殊字符(防XSS) local function escape_html(str) if type(str) ~= "string" then return "" end local replacements = { @@ -58,155 +92,168 @@ local function escape_html(str) return (str:gsub("[&<>'\"]", replacements)) end --- 登录页面HTML模板 +local function format_cookie_expires(max_age) + if max_age <= 0 then return "" end + local expires_time = ngx_time() + max_age + return os.date("!%a, %d %b %Y %H:%M:%S GMT", expires_time) +end + +local function create_cookie_header(name, value, max_age, secure) + local parts = { + string.format("%s=%s", name, value), + "Path=/", + "HttpOnly", + "SameSite=Lax" + } + if secure then table.insert(parts, "Secure") end + if max_age and max_age > 0 then + table.insert(parts, string.format("Max-Age=%d", max_age)) + local expires = format_cookie_expires(max_age) + if expires ~= "" then table.insert(parts, string.format("Expires=%s", expires)) end + end + return table.concat(parts, "; ") +end + +local function clear_cookie_header(name, secure) + local parts = { + string.format("%s=", name), + "Path=/", + "HttpOnly", + "SameSite=Lax", + "Max-Age=0", + "Expires=Thu, 01 Jan 1970 00:00:00 GMT" + } + if secure then table.insert(parts, "Secure") end + return table.concat(parts, "; ") +end + local function get_login_page(req_uri, error_message, site_name) - local escaped_error_message = escape_html(tostring(error_message or "")) + local escaped_error = escape_html(tostring(error_message or "")) local form_action = escape_html(tostring(req_uri or "/")) local site_title = site_name and escape_html(site_name) or "安全验证" - - return [[ - + return [[ - - - ]] .. site_title .. [[ - - - + + + +]] .. site_title .. [[ + - -
-
-
- - - - ]] .. site_title .. [[ -
-
-
-
- - - - -
-

身份验证

-

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

-
- ]] .. (escaped_error_message ~= "" and '
' .. escaped_error_message .. '
' or "") .. [[ -
-
- - -
-
- - -
- -
-
-

如果您遇到问题,请联系管理员

-
-
-
-
+ +
+
+
+
]] .. site_title .. [[
+
+
请输入账号密码继续访问
+]] .. (escaped_error ~= "" and '
' .. escaped_error .. '
' or "") .. [[ +
+
+
+ +
+ +
+
+
- - ]] +]] end --- 解析Cookie头 local function parse_cookies(cookie_header) if not cookie_header then return {} end - local cookies = {} for cookie in cookie_header:gmatch("[^;]+") do local key, value = cookie:match("^%s*([^=%s]+)%s*=%s*([^;]*)") - if key and value then - cookies[key] = value - end + if key and value then cookies[key] = value end end return cookies end --- 获取当前站点的认证配置 local function get_site_auth(host) for _, config in ipairs(site_auth_config) do if host:lower() == config[1]:lower() then - return { - username = config[2], - password = config[3], - site_name = config[1] - } + return { username = config[2], password = config[3], site_name = config[1], cookie_duration = config[4] or 0 } end end return nil end --- 校验登录凭证 local function validate_login(waf, auth_config) - if not waf.form or not waf.form["FORM"] then - return false - end - + if not waf.form or not waf.form["FORM"] then return false end local form = waf.form["FORM"] - if form then - local username = form["username"] - local password = form["password"] - return username == auth_config.username and password == auth_config.password - end - return false + return form["username"] == auth_config.username and form["password"] == auth_config.password end --- 检查API白名单 local function check_whitelist(host, uri) - if type(host) ~= "string" or type(uri) ~= "string" then - return false - end - + if type(host) ~= "string" or type(uri) ~= "string" then return false end host = host:lower() uri = uri:lower() - for _, rule in ipairs(api_whitelist) do local rule_host, path_prefix = rule:match("^([^/]+)/(.*)$") - if not rule_host then - rule_host = rule - path_prefix = "" + if not rule_host then rule_host = rule; path_prefix = "" end + if host == rule_host:lower() and (path_prefix == "" or uri:find("/"..path_prefix, 1, true) == 1) then return true end + end + return false +end + +local function get_header_value(waf, header_name) + if not waf or not waf.reqHeaders or type(header_name) ~= "string" then return nil end + local direct = waf.reqHeaders[header_name] + if direct then return direct end + local lower_name = header_name:lower() + for k, v in pairs(waf.reqHeaders) do + if type(k) == "string" and k:lower() == lower_name then + return v end - - if host == rule_host:lower() then - if path_prefix == "" or uri:find("/"..path_prefix, 1, true) == 1 then + end + return nil +end + +local function header_value_match(actual, expected, case_sensitive) + if actual == nil or expected == nil then return false end + if type(actual) == "table" then actual = actual[1] end + actual = tostring(actual) + expected = tostring(expected) + if case_sensitive == false then + return actual:lower() == expected:lower() + end + return actual == expected +end + +local function check_header_allowlist(waf, host) + if not waf or type(host) ~= "string" then return false end + local lower_host = host:lower() + for _, rule in ipairs(header_allowlist) do + local rule_host = tostring(rule.host or "*"):lower() + if rule.enabled ~= false and (rule_host == "*" or rule_host == lower_host) then + local actual = get_header_value(waf, rule.header) + if header_value_match(actual, rule.value, rule.case_sensitive) then return true end end @@ -214,88 +261,114 @@ local function check_whitelist(host, uri) return false end --- 创建安全Cookie -local function create_secure_cookie(session_id, scheme) - return string.format("%s=%s; Path=/; HttpOnly; SameSite=Lax%s", - cookie_name, - session_id, - scheme == "https" and "; Secure" or "" - ) -end - --- 增强型获取Cookie -local function get_auth_cookie(waf) - if waf.cookies and waf.cookies[cookie_name] then - return waf.cookies[cookie_name] - end - +local function get_cookie_value(waf, name) + if waf.cookies and waf.cookies[name] then return waf.cookies[name] end if waf.reqHeaders then local cookie_header = waf.reqHeaders["Cookie"] or waf.reqHeaders["cookie"] - if cookie_header then - local cookies = parse_cookies(cookie_header) - return cookies[cookie_name] - end + if cookie_header then return parse_cookies(cookie_header)[name] end end - local ngx_cookie = ngx.var.http_cookie - if ngx_cookie then - local cookies = parse_cookies(ngx_cookie) - return cookies[cookie_name] - end - + if ngx_cookie then return parse_cookies(ngx_cookie)[name] end return nil end --- 智能会话续期机制 -local function renew_session_if_needed(session_key, expire_time) - local current_time = ngx_time() - local remaining_time = expire_time - current_time - - if remaining_time < session_duration * renew_threshold then - local new_expire_time = current_time + session_duration - ngx_kv.db:set(session_key, new_expire_time, session_duration) - return new_expire_time +local function renew_session_if_needed(session_key, expire_time, session_duration) + local remaining = expire_time - ngx_time() + if remaining < session_duration * renew_threshold then + local new_expire = ngx_time() + session_duration + local ok, err = ngx_kv.db:set(session_key, new_expire, session_duration) + if not ok then + ngx_log(ngx_ERR, "auth-plugin: 续期会话失败: ", err or "unknown") + return expire_time, false + end + return new_expire, true + end + return expire_time, false +end + +local function append_set_cookie(cookie_value) + if not cookie_value then return end + local existing = ngx.header["Set-Cookie"] + if not existing then + ngx.header["Set-Cookie"] = cookie_value + return + end + if type(existing) == "table" then + table.insert(existing, cookie_value) + ngx.header["Set-Cookie"] = existing + return + end + ngx.header["Set-Cookie"] = { existing, cookie_value } +end + +local function block_login_banned_ip(waf) + waf.msg = "IP因登录失败次数过多已被拦截" + waf.rule_id = 10001 + waf.deny = true + ngx_kv.ipBlock:incr(waf.ip, 1, 0) + ngx_exit(403) + return true, true +end + +-- <--- 主逻辑 ---> + +function _M.resp_header_post_filter(waf) + local renew_cookie = ngx.ctx.auth_plugin_renew_cookie + if not renew_cookie then + return end - - return expire_time + + append_set_cookie(create_cookie_header( + renew_cookie.name, + renew_cookie.value, + renew_cookie.max_age, + renew_cookie.secure + )) end --- 请求阶段后过滤器 function _M.req_post_filter(waf) + if not waf then return end + local host = tostring(waf.host or "") local req_uri = tostring(waf.uri or "") local method = tostring(waf.method or "") - + local is_https = (waf.scheme == "https") local auth_config = get_site_auth(host) - if not auth_config then - return - end + if not auth_config then return end + if not waf.ip or waf.ip == "" then return end - if check_whitelist(host, req_uri) then - return - end + local session_duration = auth_config.cookie_duration > 0 and auth_config.cookie_duration or default_session_duration + local is_session_cookie = (auth_config.cookie_duration == 0) - local session_cookie = get_auth_cookie(waf) - + if ipm and ipm:match(waf.ip) then return end + if check_whitelist(host, req_uri) then return end + if check_header_allowlist(waf, host) then return end + + -- 检查已有会话 + local session_cookie = get_cookie_value(waf, cookie_name) if session_cookie then - local session_key = session_prefix .. tostring(session_cookie) + local session_key = session_prefix .. session_cookie local session_data = ngx_kv.db:get(session_key) - if session_data then - local expire_time - local session_valid = false - + local expire_time, valid = nil, false if type(session_data) == "number" then expire_time = session_data - session_valid = expire_time > ngx_time() + valid = expire_time > ngx_time() elseif session_data == true then expire_time = ngx_time() + session_duration ngx_kv.db:set(session_key, expire_time, session_duration) - session_valid = true + valid = true end - - if session_valid then - renew_session_if_needed(session_key, expire_time) + if valid then + local _, renewed = renew_session_if_needed(session_key, expire_time, session_duration) + if renewed and not is_session_cookie then + ngx.ctx.auth_plugin_renew_cookie = { + name = cookie_name, + value = session_cookie, + max_age = auth_config.cookie_duration, + secure = is_https + } + end return else ngx_kv.db:delete(session_key) @@ -303,45 +376,60 @@ function _M.req_post_filter(waf) end end - local login_attempts_key = "login_attempts:" .. tostring(waf.ip) .. ":" .. host - local login_attempts = ngx_kv.ipCache:get(login_attempts_key) or 0 - - if login_attempts >= max_login_attempts then - ngx_kv.ipBlock:incr(waf.ip, 1, 0, 600) - waf.msg = "IP因登录失败次数过多已被拦截" - waf.rule_id = 10001 - waf.deny = true - return ngx_exit(403) + -- 登录尝试次数检查 + local attempts_key = "login_attempts:" .. waf.ip .. ":" .. host + local ban_key = "login_ban:" .. waf.ip .. ":" .. host + + if ngx_kv.ipCache:get(ban_key) then + return waf.block(true) end - local show_error = false + local attempts = ngx_kv.ipCache:get(attempts_key) or 0 + + -- POST 登录处理 if method == "POST" then if validate_login(waf, auth_config) then - local new_session_id = generate_session_id() - if new_session_id then - local new_session_key = session_prefix .. new_session_id - local expire_time = ngx_time() + session_duration - ngx_kv.db:set(new_session_key, expire_time, session_duration) - ngx_kv.ipCache:delete(login_attempts_key) - - ngx.header["Set-Cookie"] = create_secure_cookie(new_session_id, waf.scheme) - return waf.redirect(req_uri) - else - show_error = true + local new_session_id = generate_random_token(32) + if not new_session_id then + ngx_log(ngx_ERR, "auth-plugin: 无法生成会话ID") + ngx.header["Set-Cookie"] = clear_cookie_header(csrf_cookie_name, is_https) + ngx.header["Content-Type"] = "text/html; charset=utf-8" + ngx_print(get_login_page(req_uri, "系统繁忙,请稍后重试", auth_config.site_name)) + return ngx_exit(ngx.HTTP_OK) end - else - login_attempts = login_attempts + 1 - ngx_kv.ipCache:set(login_attempts_key, login_attempts, 3600) - show_error = true + + local new_session_key = session_prefix .. new_session_id + local expire_time = ngx_time() + session_duration + local ok, err = ngx_kv.db:set(new_session_key, expire_time, session_duration) + if not ok then + ngx_log(ngx_ERR, "auth-plugin: 保存会话失败: ", err or "unknown") + ngx.header["Set-Cookie"] = clear_cookie_header(csrf_cookie_name, is_https) + ngx.header["Content-Type"] = "text/html; charset=utf-8" + ngx_print(get_login_page(req_uri, "系统繁忙,请稍后重试", auth_config.site_name)) + return ngx_exit(ngx.HTTP_OK) + end + ngx_kv.ipCache:delete(attempts_key) + ngx_kv.ipCache:delete(ban_key) + local session_header = create_cookie_header(cookie_name, new_session_id, auth_config.cookie_duration, is_https) + local csrf_clear = clear_cookie_header(csrf_cookie_name, is_https) + ngx.header["Set-Cookie"] = { session_header, csrf_clear } + return waf.redirect(req_uri) + end + + attempts = attempts + 1 + if attempts >= max_login_attempts then + ngx_kv.ipCache:set(ban_key, 1, login_ban_duration, 2) + ngx_kv.ipCache:delete(attempts_key) + return block_login_banned_ip(waf) end + ngx_kv.ipCache:set(attempts_key, attempts, login_attempt_window) end + -- 显示登录页面 + ngx.header["Set-Cookie"] = clear_cookie_header(csrf_cookie_name, is_https) ngx.header["Content-Type"] = "text/html; charset=utf-8" - if show_error then - ngx_print(get_login_page(req_uri, "用户名或密码错误,请重试。", auth_config.site_name)) - else - ngx_print(get_login_page(req_uri, nil, auth_config.site_name)) - end + local error_msg = (method == "POST") and "用户名或密码错误" or nil + ngx_print(get_login_page(req_uri, error_msg, auth_config.site_name)) return ngx_exit(ngx.HTTP_OK) end diff --git a/plugins/third_party/cc-protection.lua b/plugins/third_party/cc-protection.lua new file mode 100644 index 0000000..6b28755 --- /dev/null +++ b/plugins/third_party/cc-protection.lua @@ -0,0 +1,246 @@ +--- +--- CC防护插件 +--- 集成封禁黑名单、回源熔断、IP全局限速、路径限速与站点限速 +--- + +local ngx = ngx +local ngx_exit = ngx.exit +local ngx_kv = ngx.shared + +local _M = { + version = 1.8, + name = "cc-protection", + priority = 100 +} + +-- <--- 配置参数 ---> + +-- CC 防护总开关;false 时整个插件不执行任何 CC 检测 +local enableCCProtection = true + +-- 回源熔断开关;全局统计所有准备回源请求总数,超限后临时阻断所有后续请求 +local enableOriginCircuitBreaker = true + +-- IP 全局限速开关;按 IP 统计所有站点的总请求频率,作为粗过滤 +local enableGlobalRateLimit = true + +-- 精准路径限速开关;对指定 Host + Path 单独限速 +local enablePathRateLimit = true + +-- 站点频率限制开关;按 Host 独立统计请求频率 +local enableSiteRateLimit = true + +-- 回源熔断配置;纯全局计数,不区分 IP、Host、Path,用于保护源站总容量 +local originCircuitBreaker = { + enabled = true, -- 当前配置项开关 + threshold = 3000, -- 全局时间窗口内允许的最大准备回源请求总数 + timeWindow = 10, -- 统计时间窗口,单位为秒 + breakDuration = 30, -- 全局熔断持续时间,单位为秒 + countStatic = true -- 是否统计静态资源请求;true 表示所有准备回源请求都计入 +} + +-- IP 全局限速配置;跨站点按 IP 统计所有请求总数,未超限时继续执行后续精细策略 +local globalRateLimit = { + enabled = true, -- 当前配置项开关 + threshold = 1000, -- 时间窗口内允许的最大请求数 + timeWindow = 30, -- 统计时间窗口,单位为秒 + banDuration = 3600, -- 超限后的封禁时间,单位为秒 + countStatic = false -- 是否统计静态资源请求;false 表示只统计动态请求 +} + +-- 站点默认频率限制配置;未单独配置的站点使用此配置 +local siteDefault = { + enabled = true, -- 当前配置项开关 + threshold = 120, -- 时间窗口内允许的最大请求数 + timeWindow = 60, -- 统计时间窗口,单位为秒 + banDuration = 3600, -- 超限后的封禁时间,单位为秒 + countStatic = false -- 是否统计静态资源请求;false 表示只统计动态请求 +} + +-- 站点频率限制配置;key 为访问域名,未配置的域名使用 siteDefault +local siteConfigs = { + -- ["example.com"] = { + -- enabled = true, + -- threshold = 800, + -- timeWindow = 60, + -- banDuration = 3600, + -- countStatic = false + -- }, + -- ["api.example.com"] = { + -- enabled = true, + -- threshold = 500, + -- timeWindow = 60, + -- banDuration = 3600, + -- countStatic = false + -- } +} + +-- 精准路径限速配置;匹配指定 Host 和 Path 前缀后使用独立频率限制 +local pathRules = { + -- { + -- enabled = true, + -- host = "api.example.com", + -- path = "/api/send", + -- threshold = 60, + -- timeWindow = 60, + -- banDuration = 600 + -- } +} + +-- <--- 工具函数 ---> + +local function isDynamic(waf) + return waf.isQueryString or ((waf.reqContentLength or 0) > 0) +end + +local function isBanned(banKey) + local _, flags = ngx_kv.ipCache:get(banKey) + return flags == 2 +end + +local function doDeny(waf, msg, ruleId) + waf.msg = msg + waf.rule_id = ruleId + waf.deny = true + ngx_exit(403) + return true, true +end + +local function checkRate(rateKey, banKey, limit) + local count = ngx_kv.ipCache:get(rateKey) + + if not count then + ngx_kv.ipCache:set(rateKey, 1, limit.timeWindow, 1) + return false + end + + local newCount = ngx_kv.ipCache:incr(rateKey, 1) + + if newCount and newCount > limit.threshold then + ngx_kv.ipCache:set(banKey, 1, limit.banDuration, 2) + return true + end + + return false +end + +local function checkOriginCircuitBreaker(waf) + if not enableOriginCircuitBreaker or not originCircuitBreaker.enabled then + return + end + + local breakKey = "cc-break:origin" + local rateKey = "cc-origin:rate" + + if isBanned(breakKey) then + return waf.block(true) + end + + if not originCircuitBreaker.countStatic and not isDynamic(waf) then + return + end + + local count = ngx_kv.ipCache:get(rateKey) + + if not count then + ngx_kv.ipCache:set(rateKey, 1, originCircuitBreaker.timeWindow, 1) + return + end + + local newCount = ngx_kv.ipCache:incr(rateKey, 1) + + if newCount and newCount > originCircuitBreaker.threshold then + ngx_kv.ipCache:set(breakKey, 1, originCircuitBreaker.breakDuration, 2) + return doDeny(waf, "回源请求总量超限", 10014) + end +end + +-- <--- 主逻辑 ---> + +function _M.req_pre_filter(waf) + if not enableCCProtection then + return + end + + if not waf or not waf.ip or waf.ip == "" then + return + end + + local ip = waf.ip + local host = (waf.host or ""):lower() + local uri = (waf.uri or ""):lower() + + if host == "" then + return + end + + local originResult = checkOriginCircuitBreaker(waf) + if originResult then + return originResult + end + + if enableGlobalRateLimit and globalRateLimit.enabled then + local gBanKey = "cc-ban:global:" .. ip + + if isBanned(gBanKey) then + return waf.block(true) + end + + if globalRateLimit.countStatic or isDynamic(waf) then + local gRateKey = "cc-rate:global:" .. ip + + if checkRate(gRateKey, gBanKey, globalRateLimit) then + return doDeny(waf, "IP全局频率超限", 10011) + end + end + end + + if enablePathRateLimit then + for _, rule in ipairs(pathRules) do + if rule.enabled then + local ruleHost = (rule.host or ""):lower() + local rulePath = (rule.path or ""):lower() + + if host == ruleHost and rulePath ~= "" and uri:find(rulePath, 1, true) == 1 then + local pBanKey = "cc-ban:path:" .. host .. ":" .. rulePath .. ":" .. ip + + if isBanned(pBanKey) then + return waf.block(true) + end + + local pRateKey = "cc-rate:path:" .. host .. ":" .. rulePath .. ":" .. ip + + if checkRate(pRateKey, pBanKey, rule) then + return doDeny(waf, "路径限速触发", 10012) + end + + return + end + end + end + end + + if enableSiteRateLimit then + local siteConf = siteConfigs[host] or siteDefault + + if siteConf.enabled ~= false then + local sBanKey = "cc-ban:site:" .. host .. ":" .. ip + + if isBanned(sBanKey) then + return waf.block(true) + end + + if siteConf.countStatic or isDynamic(waf) then + local sRateKey = "cc-rate:site:" .. host .. ":" .. ip + + if checkRate(sRateKey, sBanKey, siteConf) then + return doDeny(waf, "站点频率超限", 10013) + end + end + end + end + + return +end + +return _M diff --git a/rules/third_party/brute-force-login-prevention.lua b/rules/third_party/brute-force-login-prevention.lua deleted file mode 100644 index 9f89cc0..0000000 --- a/rules/third_party/brute-force-login-prevention.lua +++ /dev/null @@ -1,40 +0,0 @@ ---[[ -规则名称: 登录爆破防护 -过滤阶段: 请求阶段 -危险等级: 高危 -规则描述: 针对路径中包含登录、注册等关键词的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 waf.RULE_BLOCK, "检测到登录接口发生爆破攻击,已封禁IP" - end -end - -return false diff --git a/rules/third_party/dynamic-rate-limiting-protection.lua b/rules/third_party/dynamic-rate-limiting-protection.lua deleted file mode 100644 index 2e34cca..0000000 --- a/rules/third_party/dynamic-rate-limiting-protection.lua +++ /dev/null @@ -1,140 +0,0 @@ ---[[ -规则名称: 高级动态限频防护 -过滤阶段: 请求阶段 -危险等级: 严重 -规则描述: 动态频率限制和资源防盗刷功能,支持按分类独立开关动态阈值,每类资源可设置独立检测窗口,可缓解CC攻击和资源盗刷等问题。 -作者: MCQSJ(https://github.com/MCQSJ) -更新日期: 2025/03/26 ---]] - --- 全局配置参数 -local totalWindow = 60 -- 统计站点总访问量的时间窗口,单位为秒 -local banDuration = 1440 * 60 -- 封禁时间,单位为秒(1440分钟 = 86400秒) -local totalVisitsKey = "total-visits" -- 统计全局访问量的key - --- 资源类型配置 -local resourceLimits = { - -- 大资源:压缩包、程序、视频等 - large = { - types = {"zip", "rar", "7z", "tar", "gz", "bz2", "xz", "iso", "dmg", "exe", "msi", "pkg", "apk", "deb", "rpm", "mp4", "mov", "avi", "wmv", "flv", "mkv", "webm", "m4v", "3gp"}, - baseThreshold = 5, -- 基础限制次数 - timeWindow = 300, -- 5分钟检测窗口 - enableDynamic = true -- 是否启用动态阈值 - }, - -- 小资源:图片、字体等 - small = { - types = {"png", "svg", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "ico", "psd", "ttf", "woff", "woff2", "eot", "otf"}, - baseThreshold = 30, -- 基础限制次数 - timeWindow = 60, -- 1分钟检测窗口 - enableDynamic = true -- 是否启用动态阈值 - }, - -- 常用资源:CSS、JS、JSON等 - common = { - types = {"css", "js", "json", "xml", "txt", "rtf", "csv"}, - baseThreshold = 200, -- 基础限制次数 - timeWindow = 60, -- 1分钟检测窗口 - enableDynamic = true -- 是否启用动态阈值 - }, - -- 其他资源:API请求等无后缀或未分类请求 - other = { - baseThreshold = 60, -- 基础限制次数 - timeWindow = 10, -- 10秒检测窗口 - enableDynamic = true -- 是否启用动态阈值 - } -} - --- 动态调整阈值配置(仅当分类启用动态阈值时使用) -local dynamicThresholds = { - low = { -- 低流量模式(总访问量<=200) - factor = 100, -- 宽松模式系数(百分比) - name = "宽松模式" - }, - mid = { -- 中流量模式(200<总访问量<=300) - factor = 50, -- 适中模式系数(百分比) - name = "适中模式" - }, - high = { -- 高流量模式(总访问量>300) - factor = 20, -- 紧急模式系数(百分比) - name = "紧急模式" - } -} - --- 定义总访问量挡位 -local totalVisitLimits = { - low = 200, -- 低于此处的值时为宽松模式 - mid = 300 -- 高于low低于此处值时为适中模式,高于此处值时为紧急模式 -} - -local function getFileExtension(uri) - return uri:match("^.+(%..+)$") -end - -local function getResourceConfig(uri) - local ext = getFileExtension(uri) - if ext then - ext = ext:lower() - for category, config in pairs(resourceLimits) do - if config.types then - for _, fileType in ipairs(config.types) do - if ext == "." .. fileType then - return config.baseThreshold, config.timeWindow, category, config.enableDynamic - end - end - end - end - end - local other = resourceLimits.other - return other.baseThreshold, other.timeWindow, "other", other.enableDynamic -end - -local function getDynamicFactor(visits) - if visits <= totalVisitLimits.low then - return dynamicThresholds.low.factor, dynamicThresholds.low.name - elseif visits <= totalVisitLimits.mid then - return dynamicThresholds.mid.factor, dynamicThresholds.mid.name - else - return dynamicThresholds.high.factor, dynamicThresholds.high.name - end -end - -local function calculateThreshold(base, factor, useDynamic) - if not useDynamic then - return base - end - local result = (base * factor) / 100 - if result < 1 then - return 1 - end - return result - (result % 1) -end - -local sh = waf.ipCache -local totalVisits = sh:incr(totalVisitsKey, 1, 0, totalWindow) -if not totalVisits then - return false -end - -local dynamicFactor, currentMode = getDynamicFactor(totalVisits) - -local baseThreshold, timeWindow, resourceType, enableDynamic = getResourceConfig(waf.uri) - -local finalThreshold = calculateThreshold(baseThreshold, dynamicFactor, enableDynamic) - -local ipKey = 'dynamic-protect-' .. resourceType .. '-' .. waf.ip -local count, flag = sh:get(ipKey) - -if not count then - sh:set(ipKey, 1, timeWindow, 1) -else - if flag == 2 then - return waf.block(true) - end - sh:incr(ipKey, 1) - if count + 1 >= finalThreshold then - sh:set(ipKey, count + 1, banDuration, 2) - local modeInfo = enableDynamic and ("当前模式:" .. currentMode) or "固定阈值模式" - return waf.RULE_BLOCK, "IP因请求" .. resourceType .. "资源频率过高被封禁(" .. modeInfo .. ",窗口:" .. timeWindow .. "秒,阈值:" .. finalThreshold .. ")" - end -end - -return false \ No newline at end of file diff --git a/rules/third_party/frequent-block-detection.lua b/rules/third_party/frequent-block-detection.lua deleted file mode 100644 index 1b7a5cb..0000000 --- a/rules/third_party/frequent-block-detection.lua +++ /dev/null @@ -1,31 +0,0 @@ ---[[ -规则名称: 高频攻击防护 -过滤阶段: 请求阶段 -危险等级: 高危 -规则描述: 针对发起高频率攻击的行为进行防护 -作者: 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 waf.RULE_BLOCK, "IP频繁触发拦截,已被拉黑" -end - -return false diff --git a/rules/third_party/high-frequency-error-protection.lua b/rules/third_party/high-frequency-error-protection.lua deleted file mode 100644 index 9a2e350..0000000 --- a/rules/third_party/high-frequency-error-protection.lua +++ /dev/null @@ -1,47 +0,0 @@ ---[[ -规则名称: 高频错误防护 -过滤阶段: 返回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 waf.RULE_BLOCK, "高频错误触发,IP已被封禁" - end -end - -return false