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 "") .. [[
-
-
-
-
-
+
+
+
+
+
+
+
请输入账号密码继续访问
+]] .. (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