mirror of https://github.com/Safe3/uuWAF
进行了些优化
parent
06b4c2b699
commit
202a234c11
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
---
|
||||
---
|
||||
--- 修订日期: 2024/12/20
|
||||
--- 作者: MCQSJ(https://github.com/MCQSJ)
|
||||
--- 更新日期: 2024/12/21
|
||||
---
|
||||
|
||||
local ngx = ngx
|
||||
|
@ -13,7 +12,7 @@ local ngx_today = ngx.today
|
|||
local ngx_kv = ngx.shared
|
||||
|
||||
local _M = {
|
||||
version = 1.7,
|
||||
version = 0.1,
|
||||
name = "auth-plugin" -- 插件名称
|
||||
}
|
||||
|
||||
|
@ -28,7 +27,6 @@ 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 = {
|
||||
|
@ -41,12 +39,10 @@ local function escape_html(str)
|
|||
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">
|
||||
|
@ -86,7 +82,6 @@ local function get_login_page(req_uri, error_message)
|
|||
]]
|
||||
end
|
||||
|
||||
-- 校验登录请求
|
||||
local function validate_login(waf)
|
||||
local form = waf.form["FORM"]
|
||||
if form then
|
||||
|
@ -99,13 +94,11 @@ local function validate_login(waf)
|
|||
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
|
||||
|
@ -118,48 +111,40 @@ function _M.req_post_filter(waf)
|
|||
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拉入拦截列表
|
||||
ngx_kv.ipBlock:incr(waf.ip, 1, 0)
|
||||
waf.msg = "IP因登录失败次数过多已被拦截"
|
||||
waf.rule_id = 10001
|
||||
waf.deny = true
|
||||
return ngx_exit(403) -- 返回403 Forbidden
|
||||
return ngx_exit(403)
|
||||
end
|
||||
|
||||
-- 校验会话是否已认证
|
||||
local session_key = "auth:" .. waf.ip .. ":" .. host -- 结合 IP 和 域名生成唯一会话
|
||||
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) -- 重置失败次数
|
||||
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小时
|
||||
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)) -- 使用直接返回页面
|
||||
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)) -- 使用直接返回页面
|
||||
return ngx.print(get_login_page(req_uri, nil))
|
||||
end
|
||||
end
|
||||
|
||||
-- 已经认证,继续处理后续请求
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
--[[
|
||||
规则名称: 防止爆破登录
|
||||
规则名称: 登录爆破防护
|
||||
过滤阶段: 请求阶段
|
||||
危险等级: 高危
|
||||
规则描述: 针对路径中包含登录、注册等关键词的URL特征,如果5分钟(300秒)内请求次数超过10次,则封禁该IP 1440分钟(24小时)
|
||||
规则描述: 针对路径中包含登录、注册等关键词的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 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 -- 如果路径中不包含任何特征关键词,则跳过检测
|
||||
return false
|
||||
end
|
||||
|
||||
-- 获取缓存中的数据
|
||||
local requestCount, flag = sh:get(bruteForceKey)
|
||||
if not requestCount then
|
||||
-- 初始化计数,设置5分钟(300秒)的时间窗口
|
||||
sh:set(bruteForceKey, 1, 300, 1)
|
||||
sh:set(bruteForceKey, 1, timeWindow, 1)
|
||||
else
|
||||
-- 如果标志已经为2,则IP处于封禁状态,直接拦截
|
||||
if flag == 2 then
|
||||
return waf.block(true) -- 阻断请求
|
||||
return waf.block(true)
|
||||
end
|
||||
|
||||
-- 增加非法请求次数
|
||||
sh:incr(bruteForceKey, 1)
|
||||
if requestCount + 1 > 10 then
|
||||
-- 达到爆破攻击检测阈值,标记为封禁状态,封禁时间为1440分钟(24小时)
|
||||
sh:set(bruteForceKey, requestCount + 1, 86400, 2)
|
||||
return true, "检测到登录接口发生爆破攻击,已封禁IP", true -- 日志载荷改为中文
|
||||
if requestCount + 1 > threshold then
|
||||
sh:set(bruteForceKey, requestCount + 1, banDuration, 2)
|
||||
return true, "检测到登录接口发生爆破攻击,已封禁IP", true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
--[[
|
||||
规则名称: 频繁触发攻击拦截的IP拉黑
|
||||
规则名称: 高频攻击防护
|
||||
过滤阶段: 请求阶段
|
||||
危险等级: 高危
|
||||
规则描述: 检查当前请求的客户端IP是否在最近10分钟内频繁触发(超过30次)WAF的拦截,如果是,则拉黑IP 1440分钟,并记录日志。
|
||||
注意: 因为南墙WAF特性,此规则生效对规则ID有要求,需要将此规则与南墙自带规则的第一个规则交换位置才能生效。
|
||||
规则描述: 针对发起高频率攻击的行为进行防护
|
||||
作者: MCQSJ(https://github.com/MCQSJ)
|
||||
更新日期: 2024/12/21
|
||||
!!!注意: 因为南墙WAF特性,此规则生效对规则ID有要求,需要将此规则与南墙自带规则的第一个规则交换位置才能生效!!!
|
||||
]]
|
||||
|
||||
local sh = waf.ipCache -- 键值存储库,用于存储拉黑状态
|
||||
local ip_stats = waf.ipBlock -- 查询最近被南墙拦截的IP统计,如社区版本默认存储时间为10分钟
|
||||
local ip = waf.ip
|
||||
local block_key = "blocked-" .. ip -- 用于记录IP拉黑状态的key
|
||||
-- 配置参数
|
||||
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
|
||||
|
||||
-- 如果IP已经被拉黑则直接拦截
|
||||
local c, f = sh:get(block_key)
|
||||
if c and f == 2 then
|
||||
return waf.block(true) -- 重置TCP连接,不返回任何内容
|
||||
return waf.block(true)
|
||||
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 -- 记录日志并拦截
|
||||
if recent_count and recent_count > threshold then
|
||||
sh:set(block_key, 1, banDuration, 2)
|
||||
return true, "IP频繁触发拦截,已被拉黑", true
|
||||
end
|
||||
|
||||
return false
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
规则名称: 高频错误防护
|
||||
过滤阶段: 返回HTTP头阶段
|
||||
危险等级: 中危
|
||||
规则描述: 监测频繁返回400、401、403、404、405、429、444错误,当60秒内出现这些错误10次以上,则封禁1440分钟。
|
||||
规则描述: 针对频繁触发错误的请求的行为进行防护
|
||||
作者: MCQSJ(https://github.com/MCQSJ)
|
||||
更新日期: 2024/12/21
|
||||
--]]
|
||||
|
||||
local function isSpecifiedError(status)
|
||||
-- 检查是否为指定的状态码,限定在 [400, 401, 403, 404, 405, 429, 444]
|
||||
local allowed_errors = {400, 401, 403, 404, 405, 429, 444}
|
||||
return waf.inArray(status, allowed_errors)
|
||||
end
|
||||
|
@ -16,36 +17,28 @@ local threshold = 10 -- 错误次数阈值
|
|||
local timeWindow = 60 -- 时间窗口,单位为秒
|
||||
local banDuration = 1440 * 60 -- 封禁时间,1440分钟 = 86400秒
|
||||
|
||||
-- 获取客户端IP
|
||||
local ip = waf.ip
|
||||
|
||||
-- 获取返回的HTTP状态码
|
||||
local status = waf.status
|
||||
|
||||
-- 检查当前请求是否是指定的状态码错误,不是则直接返回false
|
||||
if not isSpecifiedError(status) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- 使用 waf.ipCache 记录当前 IP 的错误次数
|
||||
local errorCache = waf.ipCache
|
||||
local errorKey = "error:" .. ip -- 定义记录错误次数的键值,以 IP 为基础区分
|
||||
local errorKey = "error:" .. ip
|
||||
|
||||
local errorCount, flag = errorCache:get(errorKey)
|
||||
|
||||
-- 若当前记录不存在,初始化记录
|
||||
if not errorCount then
|
||||
errorCache:set(errorKey, 1, timeWindow) -- 初始错误计数设置为1,并设置为60秒过期
|
||||
errorCache:set(errorKey, 1, timeWindow)
|
||||
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
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
--[[
|
||||
规则名称: 站点维护模式
|
||||
过滤阶段: 请求阶段
|
||||
危险等级: 低危
|
||||
规则描述: 将站点置为维护模式,返回“网页正在维护”的自定义页面。
|
||||
--]]
|
||||
|
||||
-- 检查是否启用维护模式的条件(可以根据需求自定义,以下为示例)
|
||||
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 -- 未启用维护模式,不拦截请求
|
Loading…
Reference in New Issue