From 88b34e9dc57da97f1bb5be25123f434393ce7c9c Mon Sep 17 00:00:00 2001 From: samwaf Date: Fri, 9 Jan 2026 09:19:22 +0800 Subject: [PATCH] feat:custom page support attack_type / http code 444 disconnected con #630 --- model/blocking_page.go | 1 + model/request/waf_blockingpage_req.go | 2 + .../waf_service/waf_blockingpage_service.go | 2 + wafdb/migrations_core.go | 28 +++ wafenginecore/error_process.go | 236 ++++++++++++------ wafenginecore/wafengine.go | 86 ++++++- 6 files changed, 273 insertions(+), 82 deletions(-) diff --git a/model/blocking_page.go b/model/blocking_page.go index 67c83bf..d79c293 100644 --- a/model/blocking_page.go +++ b/model/blocking_page.go @@ -7,6 +7,7 @@ type BlockingPage struct { baseorm.BaseOrm BlockingPageName string `json:"blocking_page_name"` //自定义拦截模板页面名称 BlockingType string `json:"blocking_type"` //自定义类型 被拦截 + AttackType string `json:"attack_type"` //攻击类型 如: cc_attack, sensitive_word, sql_injection等 HostCode string `json:"host_code"` //适用于某个网站唯一码 ResponseCode string `json:"response_code"` //响应代码 默认403 ResponseHeader string `json:"response_header"` //响应Header头信息(JSON) diff --git a/model/request/waf_blockingpage_req.go b/model/request/waf_blockingpage_req.go index c883135..043756d 100644 --- a/model/request/waf_blockingpage_req.go +++ b/model/request/waf_blockingpage_req.go @@ -5,6 +5,7 @@ import "SamWaf/model/common/request" type WafBlockingPageAddReq struct { BlockingPageName string `json:"blocking_page_name" form:"blocking_page_name"` BlockingType string `json:"blocking_type" form:"blocking_type"` + AttackType string `json:"attack_type" form:"attack_type"` HostCode string `json:"host_code" form:"host_code"` ResponseCode string `json:"response_code" form:"response_code"` ResponseHeader string `json:"response_header" form:"response_header"` @@ -14,6 +15,7 @@ type WafBlockingPageEditReq struct { Id string `json:"id"` BlockingPageName string `json:"blocking_page_name" form:"blocking_page_name"` BlockingType string `json:"blocking_type" form:"blocking_type"` + AttackType string `json:"attack_type" form:"attack_type"` HostCode string `json:"host_code" form:"host_code"` ResponseCode string `json:"response_code" form:"response_code"` ResponseHeader string `json:"response_header" form:"response_header"` diff --git a/service/waf_service/waf_blockingpage_service.go b/service/waf_service/waf_blockingpage_service.go index 951688d..d199561 100644 --- a/service/waf_service/waf_blockingpage_service.go +++ b/service/waf_service/waf_blockingpage_service.go @@ -27,6 +27,7 @@ func (receiver *WafBlockingPageService) AddApi(req request.WafBlockingPageAddReq BlockingPageName: req.BlockingPageName, BlockingType: req.BlockingType, + AttackType: req.AttackType, HostCode: req.HostCode, ResponseCode: req.ResponseCode, ResponseHeader: req.ResponseHeader, @@ -151,6 +152,7 @@ func (receiver *WafBlockingPageService) ModifyApi(req request.WafBlockingPageEdi "BlockingPageName": req.BlockingPageName, "BlockingType": req.BlockingType, + "AttackType": req.AttackType, "HostCode": req.HostCode, "ResponseCode": req.ResponseCode, "ResponseHeader": req.ResponseHeader, diff --git a/wafdb/migrations_core.go b/wafdb/migrations_core.go index 7792e23..7b03768 100644 --- a/wafdb/migrations_core.go +++ b/wafdb/migrations_core.go @@ -341,6 +341,34 @@ func RunCoreDBMigrations(db *gorm.DB) error { return nil }, }, + // 迁移9: 为 blocking_page 表添加 attack_type 字段(攻击类型) + { + ID: "202601090001_add_blocking_page_attack_type", + Migrate: func(tx *gorm.DB) error { + zlog.Info("迁移 202601090001: 为 blocking_page 表添加 attack_type 字段") + + // 检查字段是否已存在 + if tx.Migrator().HasColumn(&model.BlockingPage{}, "attack_type") { + zlog.Info("attack_type 字段已存在,跳过添加") + return nil + } + + // 添加字段,默认值为空字符串(表示通用拦截页面) + if err := tx.Migrator().AddColumn(&model.BlockingPage{}, "attack_type"); err != nil { + return fmt.Errorf("添加 attack_type 字段失败: %w", err) + } + + zlog.Info("attack_type 字段添加成功") + return nil + }, + Rollback: func(tx *gorm.DB) error { + zlog.Info("回滚 202601090001: 删除 blocking_page 表的 attack_type 字段") + if tx.Migrator().HasColumn(&model.BlockingPage{}, "attack_type") { + return tx.Migrator().DropColumn(&model.BlockingPage{}, "attack_type") + } + return nil + }, + }, }) // 执行迁移 diff --git a/wafenginecore/error_process.go b/wafenginecore/error_process.go index 6185dec..4aa6657 100644 --- a/wafenginecore/error_process.go +++ b/wafenginecore/error_process.go @@ -4,6 +4,7 @@ import ( "SamWaf/common/zlog" "SamWaf/global" "SamWaf/innerbean" + "SamWaf/model" "SamWaf/model/wafenginmodel" "SamWaf/utils" "bytes" @@ -32,8 +33,8 @@ func renderTemplate(templateContent string, data map[string]interface{}) ([]byte return renderedCode.Bytes(), nil } -// EchoErrorInfo ruleName 对内记录 blockInfo 对外展示 -func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean *innerbean.WebLog, ruleName string, blockInfo string, hostsafe *wafenginmodel.HostSafe, globalHostSafe *wafenginmodel.HostSafe, isLog bool) { +// EchoErrorInfo ruleName 对内记录 blockInfo 对外展示 attackType 攻击类型 +func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean *innerbean.WebLog, ruleName string, blockInfo string, hostsafe *wafenginmodel.HostSafe, globalHostSafe *wafenginmodel.HostSafe, isLog bool, attackType string) { resBytes := []byte("") var responseCode int = 403 @@ -42,9 +43,41 @@ func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean *innerbean "SAMWAF_BLOCK_INFO": blockInfo, } - // 处理 hostsafe 的模板 - // response_code 为 403 的配置 - blockingPage, ok := hostsafe.BlockingPage["403"] + // 优先根据攻击类型查找对应的拦截页面 + var blockingPage model.BlockingPage + var ok bool + + // 1. 优先查找网站级别的攻击类型专属拦截页面 + if attackType != "" { + for _, page := range hostsafe.BlockingPage { + if page.AttackType == attackType { + blockingPage = page + ok = true + break + } + } + } + + // 2. 如果没找到攻击类型专属页面,尝试使用响应码为403的通用拦截页面 + if !ok { + blockingPage, ok = hostsafe.BlockingPage["403"] + } + + // 3. 如果网站级别没找到,查找全局级别的攻击类型专属页面 + if !ok && attackType != "" { + for _, page := range globalHostSafe.BlockingPage { + if page.AttackType == attackType { + blockingPage = page + ok = true + break + } + } + } + + // 4. 如果还没找到,使用全局的403拦截页面 + if !ok { + blockingPage, ok = globalHostSafe.BlockingPage["403"] + } if ok { // 设置 HTTP header @@ -72,42 +105,50 @@ func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean *innerbean responseCode = code } } else { - // 检查全局配置 - globalBlockingPage, ok := globalHostSafe.BlockingPage["403"] - if ok { - // 处理 globalHostSafe 的模板 - // 设置 HTTP header - var headers []map[string]string - if err := json.Unmarshal([]byte(globalBlockingPage.ResponseHeader), &headers); err == nil { - for _, header := range headers { - if name, ok := header["name"]; ok { - if value, ok := header["value"]; ok && value != "" { - w.Header().Set(name, value) - } - } - } - } - - // 渲染模板 - renderedBytes, err := renderTemplate(globalBlockingPage.ResponseContent, renderData) - if err == nil { - resBytes = renderedBytes - } else { - resBytes = []byte(globalBlockingPage.ResponseContent) - } - // 设置响应码 - if code, err := strconv.Atoi(globalBlockingPage.ResponseCode); err == nil { - responseCode = code - } + // 默认的阻止页面 + renderedBytes, err := renderTemplate(global.GLOBAL_DEFAULT_BLOCK_INFO, renderData) + if err == nil { + resBytes = renderedBytes } else { - // 默认的阻止页面 - renderedBytes, err := renderTemplate(global.GLOBAL_DEFAULT_BLOCK_INFO, renderData) + resBytes = []byte(global.GLOBAL_DEFAULT_BLOCK_INFO) + } + } + + // 特殊处理444状态码:直接关闭连接,不返回任何内容 + if responseCode == 444 { + // 444状态码表示关闭连接,不返回任何响应 + if hj, ok := w.(http.Hijacker); ok { + conn, _, err := hj.Hijack() if err == nil { - resBytes = renderedBytes - } else { - resBytes = []byte(global.GLOBAL_DEFAULT_BLOCK_INFO) + conn.Close() + // 记录日志 + if isLog { + go func() { + // 发送推送消息 + global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.RuleMessageInfo{ + BaseMessageInfo: innerbean.BaseMessageInfo{OperaType: "命中保护规则", Server: global.GWAF_CUSTOM_SERVER_NAME}, + Domain: weblogbean.HOST, + RuleInfo: ruleName, + Ip: fmt.Sprintf("%s (%s)", weblogbean.SRC_IP, utils.GetCountry(weblogbean.SRC_IP)), + }) + }() + + datetimeNow := time.Now() + weblogbean.TimeSpent = datetimeNow.UnixNano()/1e6 - weblogbean.UNIX_ADD_TIME + weblogbean.RES_BODY = "444 Connection Closed" + weblogbean.RULE = ruleName + weblogbean.ACTION = "阻止" + weblogbean.STATUS = "连接关闭" + weblogbean.STATUS_CODE = 444 + weblogbean.TASK_FLAG = 1 + weblogbean.GUEST_IDENTIFICATION = "可疑用户" + global.GQEQUE_LOG_DB.Enqueue(weblogbean) + } + return } } + // 如果无法劫持连接,则降级为正常的403响应 + responseCode = 403 } w.WriteHeader(responseCode) @@ -135,15 +176,15 @@ func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean *innerbean weblogbean.RULE = ruleName weblogbean.ACTION = "阻止" weblogbean.STATUS = "阻止访问" - weblogbean.STATUS_CODE = 403 + weblogbean.STATUS_CODE = responseCode weblogbean.TASK_FLAG = 1 weblogbean.GUEST_IDENTIFICATION = "可疑用户" global.GQEQUE_LOG_DB.Enqueue(weblogbean) } } -// EchoResponseErrorInfo ruleName 对内记录 blockInfo 对外展示 -func EchoResponseErrorInfo(resp *http.Response, weblogbean *innerbean.WebLog, ruleName string, blockInfo string, hostsafe *wafenginmodel.HostSafe, globalHostSafe *wafenginmodel.HostSafe, isLog bool) { +// EchoResponseErrorInfo ruleName 对内记录 blockInfo 对外展示 attackType 攻击类型 +func EchoResponseErrorInfo(resp *http.Response, weblogbean *innerbean.WebLog, ruleName string, blockInfo string, hostsafe *wafenginmodel.HostSafe, globalHostSafe *wafenginmodel.HostSafe, isLog bool, attackType string) { resBytes := []byte("") var responseCode int = 403 @@ -152,9 +193,41 @@ func EchoResponseErrorInfo(resp *http.Response, weblogbean *innerbean.WebLog, ru "SAMWAF_BLOCK_INFO": blockInfo, } - // 处理 hostsafe 的模板 - // 优先使用 response_code 为 403 的配置,兼容旧版本的 other_block - blockingPage, ok := hostsafe.BlockingPage["403"] + // 优先根据攻击类型查找对应的拦截页面 + var blockingPage model.BlockingPage + var ok bool + + // 1. 优先查找网站级别的攻击类型专属拦截页面 + if attackType != "" { + for _, page := range hostsafe.BlockingPage { + if page.AttackType == attackType { + blockingPage = page + ok = true + break + } + } + } + + // 2. 如果没找到攻击类型专属页面,尝试使用响应码为403的通用拦截页面 + if !ok { + blockingPage, ok = hostsafe.BlockingPage["403"] + } + + // 3. 如果网站级别没找到,查找全局级别的攻击类型专属页面 + if !ok && attackType != "" { + for _, page := range globalHostSafe.BlockingPage { + if page.AttackType == attackType { + blockingPage = page + ok = true + break + } + } + } + + // 4. 如果还没找到,使用全局的403拦截页面 + if !ok { + blockingPage, ok = globalHostSafe.BlockingPage["403"] + } if ok { // 设置 HTTP header @@ -182,45 +255,50 @@ func EchoResponseErrorInfo(resp *http.Response, weblogbean *innerbean.WebLog, ru responseCode = code } } else { - // 检查全局配置 - globalBlockingPage, ok := globalHostSafe.BlockingPage["403"] - - if ok { - // 处理 globalHostSafe 的模板 - // 设置 HTTP header - var headers []map[string]string - if err := json.Unmarshal([]byte(globalBlockingPage.ResponseHeader), &headers); err == nil { - for _, header := range headers { - if name, ok := header["name"]; ok { - if value, ok := header["value"]; ok && value != "" { - resp.Header.Set(name, value) - } - } - } - } - - // 渲染模板 - renderedBytes, err := renderTemplate(globalBlockingPage.ResponseContent, renderData) - if err == nil { - resBytes = renderedBytes - } else { - resBytes = []byte(globalBlockingPage.ResponseContent) - } - // 设置响应码 - if code, err := strconv.Atoi(globalBlockingPage.ResponseCode); err == nil { - responseCode = code - } + // 默认的阻止页面 + renderedBytes, err := renderTemplate(global.GLOBAL_DEFAULT_BLOCK_INFO, renderData) + if err == nil { + resBytes = renderedBytes } else { - // 默认的阻止页面 - renderedBytes, err := renderTemplate(global.GLOBAL_DEFAULT_BLOCK_INFO, renderData) - if err == nil { - resBytes = renderedBytes - } else { - resBytes = []byte(global.GLOBAL_DEFAULT_BLOCK_INFO) - } + resBytes = []byte(global.GLOBAL_DEFAULT_BLOCK_INFO) } } + // 特殊处理444状态码:直接关闭连接 + if responseCode == 444 { + // 444状态码表示关闭连接,设置空body + resp.StatusCode = 444 + resp.Status = "444 Connection Closed Without Response" + resp.Body = io.NopCloser(bytes.NewBuffer([]byte{})) + resp.ContentLength = 0 + resp.Header.Set("Content-Length", "0") + // 不设置Content-Type,让连接直接关闭 + + if isLog { + go func() { + // 发送推送消息 + global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.RuleMessageInfo{ + BaseMessageInfo: innerbean.BaseMessageInfo{OperaType: "命中保护规则", Server: global.GWAF_CUSTOM_SERVER_NAME}, + Domain: weblogbean.HOST, + RuleInfo: ruleName, + Ip: fmt.Sprintf("%s (%s)", weblogbean.SRC_IP, utils.GetCountry(weblogbean.SRC_IP)), + }) + }() + + datetimeNow := time.Now() + weblogbean.TimeSpent = datetimeNow.UnixNano()/1e6 - weblogbean.UNIX_ADD_TIME + weblogbean.RES_BODY = "444 Connection Closed" + weblogbean.RULE = ruleName + weblogbean.ACTION = "阻止" + weblogbean.STATUS = "连接关闭" + weblogbean.STATUS_CODE = 444 + weblogbean.TASK_FLAG = 1 + weblogbean.GUEST_IDENTIFICATION = "可疑用户" + global.GQEQUE_LOG_DB.Enqueue(weblogbean) + } + return + } + resp.StatusCode = responseCode resp.Body = io.NopCloser(bytes.NewBuffer(resBytes)) @@ -247,7 +325,7 @@ func EchoResponseErrorInfo(resp *http.Response, weblogbean *innerbean.WebLog, ru weblogbean.RULE = ruleName weblogbean.ACTION = "阻止" weblogbean.STATUS = "阻止访问" - weblogbean.STATUS_CODE = 403 + weblogbean.STATUS_CODE = responseCode weblogbean.TASK_FLAG = 1 weblogbean.GUEST_IDENTIFICATION = "可疑用户" global.GQEQUE_LOG_DB.Enqueue(weblogbean) diff --git a/wafenginecore/wafengine.go b/wafenginecore/wafengine.go index 1c3d29c..9d0c1b4 100644 --- a/wafenginecore/wafengine.go +++ b/wafenginecore/wafengine.go @@ -77,6 +77,84 @@ func (waf *WafEngine) Error() string { zlog.Error(fmt.Sprintf(fs)) return fmt.Sprintf(fs) } + +// inferAttackType 根据检测规则标题推断攻击类型 +func inferAttackType(ruleTitle string) string { + ruleTitle = strings.ToLower(ruleTitle) + + // CC攻击 + if strings.Contains(ruleTitle, "cc") || strings.Contains(ruleTitle, "频次") || strings.Contains(ruleTitle, "rate limit") { + return "cc_attack" + } + + // SQL注入 + if strings.Contains(ruleTitle, "sql") || strings.Contains(ruleTitle, "注入") { + return "sql_injection" + } + + // XSS攻击 + if strings.Contains(ruleTitle, "xss") || strings.Contains(ruleTitle, "跨站") { + return "xss_attack" + } + + // 扫描工具 + if strings.Contains(ruleTitle, "scan") || strings.Contains(ruleTitle, "扫描") { + return "scan_tool" + } + + // RCE远程代码执行 + if strings.Contains(ruleTitle, "rce") || strings.Contains(ruleTitle, "代码执行") || strings.Contains(ruleTitle, "命令执行") { + return "rce_attack" + } + + // 目录穿越 + if strings.Contains(ruleTitle, "traversal") || strings.Contains(ruleTitle, "穿越") || strings.Contains(ruleTitle, "目录") { + return "dir_traversal" + } + + // Bot爬虫 + if strings.Contains(ruleTitle, "bot") || strings.Contains(ruleTitle, "爬虫") { + return "bot_attack" + } + + // 敏感词 + if strings.Contains(ruleTitle, "sensitive") || strings.Contains(ruleTitle, "敏感词") { + return "sensitive_word" + } + + // IP黑名单 + if strings.Contains(ruleTitle, "ip") && (strings.Contains(ruleTitle, "黑名单") || strings.Contains(ruleTitle, "block") || strings.Contains(ruleTitle, "deny")) { + return "ip_blocked" + } + + // URL黑名单 + if strings.Contains(ruleTitle, "url") && (strings.Contains(ruleTitle, "黑名单") || strings.Contains(ruleTitle, "block") || strings.Contains(ruleTitle, "deny")) { + return "url_blocked" + } + + // 防盗链 + if strings.Contains(ruleTitle, "leech") || strings.Contains(ruleTitle, "防盗链") || strings.Contains(ruleTitle, "hotlink") { + return "anti_leech" + } + + // 自定义规则 + if strings.Contains(ruleTitle, "rule") || strings.Contains(ruleTitle, "规则") { + return "custom_rule" + } + + // OWASP规则 + if strings.Contains(ruleTitle, "owasp") { + return "owasp_rule" + } + + // 插件拦截 + if strings.Contains(ruleTitle, "plugin") || strings.Contains(ruleTitle, "插件") { + return "plugin_block" + } + + // 默认返回通用拦截类型 + return "" +} func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { innerLogName := "WafEngine ServeHTTP" global.IncrementQPS() // 使用统一的QPS增量函数 @@ -358,7 +436,7 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { Duration: 0, // CC封禁时长由配置决定 Time: time.Now().Format("2006-01-02 15:04:05"), }) - EchoErrorInfo(w, r, &weblogbean, "", "当前IP由于访问频次太高暂时无法访问", hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], false) + EchoErrorInfo(w, r, &weblogbean, "", "当前IP由于访问频次太高暂时无法访问", hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], false, "cc_attack") return } @@ -398,7 +476,9 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { return false } else { decrementMonitor(hostCode) - EchoErrorInfo(w, r, &weblogbean, detectionResult.Title, detectionResult.Content, hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true) + // 根据检测结果的标题推断攻击类型 + attackType := inferAttackType(detectionResult.Title) + EchoErrorInfo(w, r, &weblogbean, detectionResult.Title, detectionResult.Content, hostTarget, waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true, attackType) return true } } @@ -1025,7 +1105,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error { weblogfrist.RULE = "敏感词检测:" + string(matchBodyResult[0].Word) } else { - EchoResponseErrorInfo(resp, weblogfrist, "敏感词检测:"+string(matchBodyResult[0].Word), "敏感词内容", waf.HostTarget[host], waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true) + EchoResponseErrorInfo(resp, weblogfrist, "敏感词检测:"+string(matchBodyResult[0].Word), "敏感词内容", waf.HostTarget[host], waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true, "sensitive_word") return nil } } else {