Merge pull request #636 from samwafgo/feat_cc_444

feat:custom page support attack_type / http code 444 disconnected con
This commit is contained in:
samwafgo
2026-01-09 09:28:02 +08:00
committed by GitHub
6 changed files with 273 additions and 82 deletions
+1
View File
@@ -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)
+2
View File
@@ -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"`
@@ -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,
+28
View File
@@ -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
},
},
})
// 执行迁移
+157 -79
View File
@@ -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)
+83 -3
View File
@@ -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 {