feat:add anti leech

#138
This commit is contained in:
samwaf
2025-04-16 15:39:09 +08:00
parent fe17f3bdf3
commit 8ab7b5be31
9 changed files with 396 additions and 17 deletions

View File

@@ -36,6 +36,7 @@ type Hosts struct {
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
AntiLeechJSON string `json:"anti_leech_json"` //防盗链配置 json
}
type HostsDefense struct {
@@ -67,3 +68,12 @@ type CaptchaConfig struct {
ExpireTime int `json:"expire_time"` // 验证通过后的有效期(小时)
IPMode string `json:"ip_mode"` // IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式
}
// AntiLeechConfig 防盗链配置
type AntiLeechConfig struct {
IsEnableAntiLeech int `json:"is_enable_anti_leech"` // 是否开启防盗链 1开启 0关闭
FileTypes string `json:"file_types"` // 需要防盗链的文件类型,例如: gif|jpg|jpeg|png|bmp|swf
ValidReferers string `json:"valid_referers"` // 允许的引用来源列表,使用分号(;)分隔
Action string `json:"action"` // 对于非法引用的处理方式: redirect(重定向) 或 block(直接阻止)
RedirectURL string `json:"redirect_url"` // 重定向URL当Action为redirect时使用
}

View File

@@ -31,6 +31,7 @@ type WafHostAddReq struct {
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
AntiLeechJSON string `json:"anti_leech_json"` //防盗链配置 json
}
type WafHostDelReq struct {
@@ -70,6 +71,7 @@ type WafHostEditReq struct {
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
AntiLeechJSON string `json:"anti_leech_json"` //防盗链配置 json
}
type WafHostGuardStatusReq struct {

View File

@@ -62,6 +62,7 @@ func (receiver *WafHostService) AddApi(wafHostAddReq request.WafHostAddReq) (str
HealthyJSON: wafHostAddReq.HealthyJSON,
InsecureSkipVerify: wafHostAddReq.InsecureSkipVerify,
CaptchaJSON: wafHostAddReq.CaptchaJSON,
AntiLeechJSON: wafHostAddReq.AntiLeechJSON,
}
global.GWAF_LOCAL_DB.Create(wafHost)
return wafHost.Code, nil
@@ -114,6 +115,7 @@ func (receiver *WafHostService) ModifyApi(wafHostEditReq request.WafHostEditReq)
"HealthyJSON": wafHostEditReq.HealthyJSON,
"InsecureSkipVerify": wafHostEditReq.InsecureSkipVerify,
"CaptchaJSON": wafHostEditReq.CaptchaJSON,
"AntiLeechJSON": wafHostEditReq.AntiLeechJSON,
}
err := global.GWAF_LOCAL_DB.Debug().Model(model.Hosts{}).Where("CODE=?", wafHostEditReq.CODE).Updates(hostMap).Error

View File

@@ -0,0 +1,187 @@
package wafenginecore
import (
"SamWaf/common/zlog"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/detection"
"SamWaf/model/wafenginmodel"
"encoding/json"
"net/http"
"net/url"
"regexp"
"strings"
)
// CheckAntiLeech 检查防盗链
func (waf *WafEngine) CheckAntiLeech(r *http.Request, weblogbean *innerbean.WebLog, formValue url.Values, hostTarget *wafenginmodel.HostSafe, globalHostTarget *wafenginmodel.HostSafe) detection.Result {
result := detection.Result{
JumpGuardResult: false,
IsBlock: false,
Title: "",
Content: "",
}
antiLeechConfig := model.AntiLeechConfig{
IsEnableAntiLeech: 0,
FileTypes: "",
ValidReferers: "",
Action: "",
RedirectURL: "",
}
json.Unmarshal([]byte(hostTarget.Host.AntiLeechJSON), &antiLeechConfig)
// 检查是否启用了防盗链
if antiLeechConfig.IsEnableAntiLeech == 0 {
return result
}
// 检查请求的URL是否匹配需要防盗链的文件类型
if !isProtectedFileType(r.URL.Path, antiLeechConfig.FileTypes) {
return result
}
// 获取Referer
referer := r.Header.Get("Referer")
// 检查Referer是否有效
hostWithoutPort := r.Host
if strings.Contains(hostWithoutPort, ":") {
hostWithoutPort, _, _ = strings.Cut(hostWithoutPort, ":")
}
// 收集所有server_names
serverNames := []string{hostWithoutPort}
if hostTarget != nil {
bindMoreHost := hostTarget.Host.BindMoreHost
if bindMoreHost != "" {
lines := strings.Split(bindMoreHost, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
serverNames = append(serverNames, line)
}
}
}
}
if isValidReferer(referer, serverNames, strings.Split(antiLeechConfig.ValidReferers, ";")) {
return result
}
// 如果Referer无效根据配置的Action进行处理
result.IsBlock = true
result.Title = "防盗链保护"
if antiLeechConfig.Action == "redirect" && antiLeechConfig.RedirectURL != "" {
// 防止重定向循环
redirectURL, err := url.Parse(antiLeechConfig.RedirectURL)
if err == nil {
// 处理r.Host和redirectURL.Host去掉端口仅比较纯域名
reqHost := r.Host
if strings.Contains(reqHost, ":") {
reqHost, _, _ = strings.Cut(reqHost, ":")
}
redirectHost := redirectURL.Host
if strings.Contains(redirectHost, ":") {
redirectHost, _, _ = strings.Cut(redirectHost, ":")
}
if reqHost == redirectHost && strings.HasPrefix(r.URL.Path, redirectURL.Path) {
result.IsBlock = true
result.Title = "防盗链保护"
result.Content = "检测到重定向循环,已阻断"
return result
}
}
// 记录日志
zlog.Debug("防盗链保护: 检测到非法引用,重定向到: " + antiLeechConfig.RedirectURL)
// 设置重定向URL
result.Content = "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"0;url=" +
antiLeechConfig.RedirectURL + "\"></head><body>Redirecting...</body></html>"
} else {
// 默认阻止访问
result.Content = "您没有权限访问此资源"
}
return result
}
// isProtectedFileType 检查URL是否匹配需要防盗链的文件类型
func isProtectedFileType(path string, fileTypes string) bool {
if fileTypes == "" {
return false
}
pattern := ".*\\.(" + fileTypes + ")$"
matched, err := regexp.MatchString(pattern, strings.ToLower(path))
if err != nil {
zlog.Error("防盗链正则匹配错误: " + err.Error())
return false
}
return matched
}
// isValidReferer 检查Referer是否有效
func isValidReferer(referer string, hosts []string, validReferers []string) bool {
// 如果没有设置有效的Referer列表则默认允许所有
if len(validReferers) == 0 {
return true
}
// 如果没有Referer检查是否允许none
if referer == "" {
return contains(validReferers, "none")
}
// 解析Referer URL
parsedReferer, err := url.Parse(referer)
if err != nil {
return contains(validReferers, "blocked")
}
refererHost := parsedReferer.Host
// 检查是否允许当前服务器名称(支持多域名)
if contains(validReferers, "server_names") {
for _, h := range hosts {
if refererHost == h {
return true
}
}
}
// 检查是否匹配其他有效的Referer模式
for _, validReferer := range validReferers {
// 跳过特殊关键字
if validReferer == "none" || validReferer == "blocked" || validReferer == "server_names" {
continue
}
// 处理正则表达式模式 (~开头)
if strings.HasPrefix(validReferer, "~") {
pattern := strings.TrimPrefix(validReferer, "~")
matched, err := regexp.MatchString(pattern, refererHost)
if err == nil && matched {
return true
}
} else if strings.HasPrefix(validReferer, "*.") {
// 处理通配符域名 (*.example.com)
suffix := strings.TrimPrefix(validReferer, "*")
if strings.HasSuffix(refererHost, suffix) {
return true
}
} else if refererHost == validReferer {
// 完全匹配
return true
}
}
return false
}
// contains 检查字符串数组是否包含指定字符串
func contains(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}

View File

@@ -0,0 +1,177 @@
package wafenginecore
import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"encoding/json"
"net/http"
"net/url"
"testing"
)
func TestCheckAntiLeech(t *testing.T) {
// 初始化日志
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化 WAF 引擎
waf := &WafEngine{
HostTarget: make(map[string]*wafenginmodel.HostSafe),
}
// 构造防盗链配置JSON
antiLeechConfig := model.AntiLeechConfig{
IsEnableAntiLeech: 1,
FileTypes: "gif|jpg|jpeg|png|bmp|swf",
ValidReferers: "none;server_names;*.example.com;~\\.google\\.;~\\.baidu\\.",
Action: "redirect",
RedirectURL: "https://example.com/403.jpg",
}
antiLeechJSON, _ := json.Marshal(antiLeechConfig)
// 创建测试主机配置,模拟绑定多个域名
hostTarget := &wafenginmodel.HostSafe{
Host: model.Hosts{
AntiLeechJSON: string(antiLeechJSON),
BindMoreHost: "alias1.com\nalias2.com",
},
}
// 创建全局主机配置
globalHostTarget := &wafenginmodel.HostSafe{}
// 测试用例
testCases := []struct {
name string
url string
referer string
host string
expectBlocked bool
}{
{
name: "允许的Referer",
url: "/images/test.jpg",
referer: "https://sub.example.com/page",
host: "mysite.com",
expectBlocked: false,
},
{
name: "允许的搜索引擎Referer",
url: "/images/test.png",
referer: "https://www.google.com/search",
host: "mysite.com",
expectBlocked: false,
},
{
name: "无Referer但允许none",
url: "/images/test.gif",
referer: "",
host: "mysite.com",
expectBlocked: false,
},
{
name: "同站点Referer",
url: "/images/test.jpeg",
referer: "https://mysite.com/page",
host: "mysite.com",
expectBlocked: false,
},
{
name: "非法Referer",
url: "/images/test.bmp",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: true,
},
{
name: "非防盗链文件类型",
url: "/documents/test.pdf",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: false,
},
// 新增后置场景测试
{
name: "带查询参数的图片",
url: "/images/test.jpg?ver=1.2",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: true,
},
{
name: "带锚点的图片",
url: "/images/test.png#section",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: true,
},
{
name: "路径末尾带斜杠",
url: "/images/test.jpg/",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: false, // 这种情况一般不会被正则识别为图片文件
},
{
name: "文件名大写扩展名",
url: "/images/TEST.JPG",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: true,
},
{
name: "带参数和锚点的图片",
url: "/images/test.jpeg?foo=bar#anchor",
referer: "https://attacker.com/page",
host: "mysite.com",
expectBlocked: true,
},
{
name: "重定向目标再次访问",
url: "/403.jpg",
referer: "https://attacker.com/page",
host: "example.com", // 与RedirectURL主机一致
expectBlocked: true, // 推荐阻断,防止重定向循环
},
{
name: "绑定域名alias1.com同站点Referer",
url: "/images/test.jpeg",
referer: "https://alias1.com/page",
host: "alias1.com",
expectBlocked: false,
},
{
name: "绑定域名alias2.com同站点Referer",
url: "/images/test.jpeg",
referer: "https://alias2.com/page",
host: "alias2.com",
expectBlocked: false,
},
{
name: "绑定域名alias1.com非法Referer",
url: "/images/test.jpeg",
referer: "https://evil.com/page",
host: "alias1.com",
expectBlocked: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", tc.url, nil)
req.Host = tc.host
if tc.referer != "" {
req.Header.Set("Referer", tc.referer)
}
weblog := &innerbean.WebLog{
URL: tc.url,
}
result := waf.CheckAntiLeech(req, weblog, url.Values{}, hostTarget, globalHostTarget)
if result.IsBlock != tc.expectBlocked {
t.Errorf("测试 '%s' 失败: 期望阻止=%v, 实际=%v", tc.name, tc.expectBlocked, result.IsBlock)
}
})
}
}

View File

@@ -16,7 +16,7 @@ func TestCheckAllowURL(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化 WAF 引擎
waf := &WafEngine{
HostTarget: make(map[string]*wafenginmodel.HostSafe),

View File

@@ -16,7 +16,7 @@ func TestCheckDenyURL(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化 WAF 引擎
waf := &WafEngine{
HostTarget: make(map[string]*wafenginmodel.HostSafe),

View File

@@ -349,6 +349,11 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// 添加防盗链检查
if handleBlock(waf.CheckAntiLeech) {
return
}
// 验证码检测
captchaConfig := model.CaptchaConfig{
IsEnableCaptcha: 0,
@@ -1089,9 +1094,7 @@ func (waf *WafEngine) ClearCcWindowsForIP(ip string) {
if hostSafe.PluginIpRateLimiter != nil {
// 获取清理前的请求计数
var countBefore int
if hostSafe.PluginIpRateLimiter.GetRequestCount != nil {
countBefore = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
}
countBefore = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
// 清理该IP的限流记录
hostSafe.PluginIpRateLimiter.ClearWindowForIP(ip)
@@ -1099,9 +1102,7 @@ func (waf *WafEngine) ClearCcWindowsForIP(ip string) {
// 获取清理后的请求计数
var countAfter int
if hostSafe.PluginIpRateLimiter.GetRequestCount != nil {
countAfter = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
}
countAfter = hostSafe.PluginIpRateLimiter.GetRequestCount(ip)
// 打印清理前后的计数信息
zlog.Debug(fmt.Sprintf("主机 %s 的 IP %s 限流记录: 清理前=%d, 清理后=%d",

View File

@@ -114,7 +114,7 @@ func TestReplaceURLContent(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
if v := recover(); v != nil {
zlog.Error("error")
}
@@ -150,7 +150,7 @@ func TestGetOrgContent(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
@@ -274,7 +274,7 @@ func TestGetOrgContent(t *testing.T) {
}
// 调用测试函数
result, err := waf.getOrgContent(resp)
result, err := waf.getOrgContent(resp, false)
// 验证结果
if tc.expectedErr {
@@ -320,7 +320,7 @@ func TestGetOrgContentWithChunkedEncoding(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
@@ -337,7 +337,7 @@ func TestGetOrgContentWithChunkedEncoding(t *testing.T) {
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
// 调用测试函数
result, err := waf.getOrgContent(resp)
result, err := waf.getOrgContent(resp, false)
// 验证结果
if err != nil {
@@ -352,7 +352,7 @@ func TestGetOrgContentWithEmptyBody(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
@@ -365,7 +365,7 @@ func TestGetOrgContentWithEmptyBody(t *testing.T) {
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
// 调用测试函数
result, err := waf.getOrgContent(resp)
result, err := waf.getOrgContent(resp, false)
// 验证结果
if err != nil {
@@ -384,7 +384,7 @@ func TestGetOrgContentWithErrors(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
zlog.InitZLog(global.GWAF_RELEASE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
@@ -399,7 +399,7 @@ func TestGetOrgContentWithErrors(t *testing.T) {
resp.Header.Set("Content-Encoding", "gzip")
// 调用测试函数
_, err := waf.getOrgContent(resp)
_, err := waf.getOrgContent(resp, false)
// 验证结果
if err == nil {