第三方规则/插件

第三方实现的插件与规则
pull/107/head
大力丸666 2024-12-20 22:23:46 +08:00
parent d79535e1b4
commit 9085a70807
5 changed files with 379 additions and 0 deletions

165
src/plugins/third_party/auth-plugin.lua vendored Normal file
View File

@ -0,0 +1,165 @@
---
---
---
--- 修订日期: 2024/12/20
---
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 = 1.7,
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 -- 最大登录失败次数
-- 处理特殊字符函数,防止 HTML 注入
local function escape_html(str)
if not str then return "" end
local replacements = {
["&"] = "&",
["<"] = "&lt;",
[">"] = "&gt;",
['"'] = "&quot;",
["'"] = "&#39;",
}
return (str:gsub("[&<>'\"]", function(c) return replacements[c] end))
end
-- 登录页面HTML模板带错误提示信息
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 "/")
-- HTML部分拼接
return [[
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style>
/* */
* {margin: 0; padding: 0; box-sizing: border-box;}
body { min-height: 100vh; background: linear-gradient(120deg, #e0c3fc, #8ec5fc); display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.glass { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(15px); border-radius: 30px; border: 1px solid rgba(255, 255, 255, 0.3); box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15); padding: 40px; width: 90%; max-width: 480px; text-align: center; position: relative; }
h1 {color: #4a4a4a; font-size: 28px; font-weight: 600; margin-bottom: 15px;}
.error-message {color: #d9534f; font-size: 14px; margin-bottom: 20px;}
label {display: block; text-align: left; margin-bottom: 10px; font-weight: bold; color: #555;}
input[type="text"], input[type="password"] {width: 100%; padding: 12px; margin-bottom: 20px; border-radius: 15px; border: 1px solid rgba(255, 255, 255, 0.4); font-size: 14px; background: rgba(255, 255, 255, 0.2); color: #555;}
input[type="submit"] {background: linear-gradient(45deg, #6e8efb, #a777e3); color: white; padding: 12px 35px; border-radius: 25px; font-weight: 500; border: none; cursor: pointer; font-size: 16px; transition: transform 0.3s ease, box-shadow 0.3s ease;}
input[type="submit"]:hover {transform: translateY(-2px); box-shadow: 0 5px 15px rgba(110, 142, 251, 0.4);}
.note {color: #666; font-size: 12px; margin-top: 20px;}
</style>
</head>
<body>
<div class="glass">
<h1></h1>
]] .. (escaped_error_message ~= "" and '<p class="error-message">' .. escaped_error_message .. '</p>' or "") .. [[
<form method="POST" action="]] .. form_action .. [[">
<label for="username"></label>
<input type="text" id="username" name="username" placeholder="输入用户名" required>
<label for="password"></label>
<input type="password" id="password" name="password" placeholder="输入密码" required>
<input type="submit" value="登录">
</form>
<p class="note">访</p>
</div>
</body>
</html>
]]
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) -- 将IP拉入拦截列表
waf.msg = "IP因登录失败次数过多已被拦截"
waf.rule_id = 10001
waf.deny = true
return ngx_exit(403) -- 返回403 Forbidden
end
-- 校验会话是否已认证
local session_key = "auth:" .. waf.ip .. ":" .. host -- 结合 IP 和 域名生成唯一会话
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) -- 失败次数保存1小时
-- 可选:输出登录失败提示(直接返回页面避免请求到原站)
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

View File

@ -0,0 +1,39 @@
--[[
:
:
:
: URL5(300)10IP 144024
--]]
local sh = waf.ipCache
local bruteForceKey = 'brute-force-login:' .. waf.ip -- 使用独立前缀标识,避免与其他规则冲突
-- 定义特征路径关键词列表
local targetPaths = { "login", "signin", "signup", "register", "reset", "passwd", "account", "user" }
-- 判断URI是否包含特征关键词
if not waf.pmMatch(waf.toLower(waf.uri), targetPaths) then
return false -- 如果路径中不包含任何特征关键词,则跳过检测
end
-- 获取缓存中的数据
local requestCount, flag = sh:get(bruteForceKey)
if not requestCount then
-- 初始化计数设置5分钟300秒的时间窗口
sh:set(bruteForceKey, 1, 300, 1)
else
-- 如果标志已经为2则IP处于封禁状态直接拦截
if flag == 2 then
return waf.block(true) -- 阻断请求返回403响应
end
-- 增加非法请求次数
sh:incr(bruteForceKey, 1)
if requestCount + 1 > 10 then
-- 达到爆破攻击检测阈值标记为封禁状态封禁时间为1440分钟24小时
sh:set(bruteForceKey, requestCount + 1, 86400, 2)
return true, "检测到登录接口发生爆破攻击已封禁IP", true -- 日志载荷改为中文
end
end
return false

View File

@ -0,0 +1,28 @@
--[[
: IP
:
:
: IP1030WAFIP 1440
: WAFID
]]
local sh = waf.ipCache -- 键值存储库,用于存储拉黑状态
local ip_stats = waf.ipBlock -- 查询最近被南墙拦截的IP统计如社区版本默认存储时间为10分钟
local ip = waf.ip
local block_key = "blocked-" .. ip -- 用于记录IP拉黑状态的key
-- 如果IP已经被拉黑则直接拦截
local c, f = sh:get(block_key)
if c and f == 2 then
return waf.block(true) -- 重置TCP连接不返回任何内容
end
-- 检查该IP在最近时间内是否频繁被拦截
local recent_count = ip_stats:get(ip)
if recent_count and recent_count > 30 then
-- 如果超过30次则拉黑IP设置1440分钟24小时
sh:set(block_key, 1, 86400, 2) -- 第三个参数86400为1440分钟单位为秒第四个参数2表示拉黑状态
return true, "IP频繁触发拦截已被拉黑", true -- 记录日志并拦截
end
return false

View File

@ -0,0 +1,53 @@
--[[
:
: HTTP
:
: 40x50x60101440
--]]
local function isCommonError(status)
-- 检查是否为40x或50x错误
return status >= 400 and status < 600
end
-- 配置参数
local threshold = 10 -- 错误次数阈值
local timeWindow = 60 -- 时间窗口,单位为秒
local banDuration = 1440 * 60 -- 封禁时间1440分钟 = 86400秒
-- 获取客户端IP
local ip = waf.ip
-- 获取返回的HTTP状态码
local status = waf.status
-- 检查当前请求是否是40x或者50x错误不是则直接返回false
if not isCommonError(status) then
return false
end
-- 使用 waf.ipCache 记录当前 IP 的错误次数
local errorCache = waf.ipCache
local errorKey = "error:" .. ip -- 定义记录错误次数的键值,以 IP 为基础区分
local errorCount, flag = errorCache:get(errorKey)
-- 若当前记录不存在,初始化记录
if not errorCount then
errorCache:set(errorKey, 1, timeWindow) -- 初始错误计数设置为1并设置为60秒过期
else
if flag == 2 then
-- 标志为2表示该IP已被封禁直接拦截即刻终止
return waf.block(true)
end
-- 累加错误计数
errorCache:incr(errorKey, 1)
if errorCount + 1 >= threshold then
-- 达到错误频率阈值标记当前IP为封禁状态
errorCache:set(errorKey, errorCount + 1, banDuration, 2)
return true, "高频错误触发IP已被封禁", true
end
end
return false

View File

@ -0,0 +1,94 @@
--[[
:
:
:
:
--]]
-- 检查是否启用维护模式的条件(可以根据需求自定义,以下为示例)
local maintenance_mode = true -- 可以通过配置文件或其他方式动态控制
if maintenance_mode then
-- 设置自定义的维护页面 HTML 内容
local maintenance_html = [[<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style>
* {margin: 0; padding: 0; box-sizing: border-box;}
body {
min-height: 100vh;
background: linear-gradient(120deg, #e0c3fc, #8ec5fc);
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.glass {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(15px);
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
padding: 40px;
width: 90%;
max-width: 480px;
text-align: center;
position: relative;
}
.icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
}
h1 {
color: #4a4a4a;
font-size: 28px;
font-weight: 600;
margin-bottom: 15px;
}
.message {
color: #666;
font-size: 16px;
line-height: 1.6;
margin: 15px 0;
}
.provider {
color: #666;
font-size: 12px;
position: absolute;
bottom: -30px;
left: 0;
right: 0;
text-align: center;
}
.provider strong {
color: #5856d6;
}
</style>
</head>
<body>
<div class="glass">
<div class="icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22c5.5-4 8-8 8-12V5l-8-3-8 3v5c0 4 2.5 8 8 12z" stroke="#6e8efb" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12l2 2 4-4" stroke="#a777e3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1></h1>
<p class="message">访</p>
<div class="provider"> <strong> WAF</strong> </div>
</div>
</body>
</html>]]
ngx.header.content_type = "text/html; charset=utf-8"
-- 输出维护页面并终止请求处理
ngx.print(maintenance_html)
return ngx.exit(ngx.HTTP_OK) -- 使用 ngx.HTTP_OK 结束请求,避免传递到源站
end
return false -- 未启用维护模式,不拦截请求