feat:sensitive check response and action

#31 #137
This commit is contained in:
samwaf
2025-02-20 14:09:10 +08:00
parent 071b5962f5
commit 38f59af0ed
21 changed files with 528 additions and 90 deletions

View File

@@ -236,7 +236,12 @@ func (w *WafLogAPi) GetAllIpTagApi(c *gin.Context) {
}
}
func GenerateRawHTTPRequest(weblog innerbean.WebLog) string {
parsedURL, err := url.Parse(weblog.URL)
reqUrl := weblog.URL
if weblog.SrcURL != nil {
reqUrl = string(weblog.SrcURL)
}
parsedURL, err := url.Parse(reqUrl)
if err != nil {
return ""
}
@@ -327,11 +332,16 @@ func GenerateCurlRequest(weblog innerbean.WebLog) string {
maskedCookies := maskSensitiveCookies(weblog.COOKIES)
reqUrl := weblog.URL
if weblog.SrcURL != nil {
reqUrl = string(weblog.SrcURL)
}
curlCommand := fmt.Sprintf(
"curl -X %s %s \\\n --url '%s' \\\n --cookie '%s' \\\n --data '%s'",
weblog.METHOD,
headerStrings,
weblog.URL,
reqUrl,
maskedCookies,
weblog.BODY,
)

View File

@@ -89,6 +89,9 @@ var (
GCACHE_WAFCACHE *cache.WafCache //cache
GCACHE_WECHAT_ACCESS string = "" //微信访问密钥
/*********HTTP相关**************/
GWAF_HTTP_SENSITIVE_REPLACE_STRING = "**" //HTTP 敏感内容替换成
/*********IP相关**************/
GCACHE_IP_CBUFF []byte // IP相关缓存
GCACHE_IP_V6_COUNTRY_CBUFF []byte // IPv6国家相关缓存

3
go.mod
View File

@@ -4,7 +4,6 @@ go 1.22.9
require (
github.com/360EntSecGroup-Skylar/excelize v1.4.1
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/bytedance/godlp v1.2.15
github.com/corazawaf/coraza/v3 v3.2.2
github.com/corazawaf/libinjection-go v0.2.2
@@ -14,6 +13,7 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/go-acme/lego/v4 v4.20.4
github.com/go-co-op/gocron v1.17.1
github.com/go-playground/assert/v2 v2.2.0
github.com/gorilla/websocket v1.5.0
github.com/hyperjumptech/grule-rule-engine v1.11.0
github.com/kardianos/service v1.2.2
@@ -21,6 +21,7 @@ require (
github.com/oschwald/geoip2-golang v1.11.0
github.com/pengge/go-wxsqlite3 v0.0.0-20231127082057-d869bc67f783
github.com/pengge/sqlitedriver v0.0.0-20231127095117-b0f000e40c2c
github.com/samwafgo/ahocorasick v1.0.0
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/viper v1.18.2

4
go.sum
View File

@@ -4,8 +4,6 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@@ -184,6 +182,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samwafgo/ahocorasick v1.0.0 h1:/PMXeYaV1lumsPdueT11aubJ95DcvYzIHyBiMxm8+TI=
github.com/samwafgo/ahocorasick v1.0.0/go.mod h1:PCU/JDpXAk9IACzdN5W1+AI5JiiByZGrG2pj4YnxzIc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=

View File

@@ -38,6 +38,7 @@ type WebLog struct {
SrcByteResBody []byte `json:"src_byte_res_body"` //返回body bytes信息
WebLogVersion int `json:"web_log_version"` //日志版本信息早期的是空和0后期实时增加
Scheme string `json:"scheme"` //HTTP 协议
SrcURL []byte `json:"src_url"` //原始url信息
}
// 在 GORM 的 Model 方法中定义复合索引

View File

@@ -1,7 +0,0 @@
package request
type WafSensitiveAddReq struct {
Type int `json:"type" form:"type"` //敏感词类型
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
}

View File

@@ -1,5 +0,0 @@
package request
type WafSensitiveDelReq struct {
Id string `json:"id" form:"id"` //唯一键
}

View File

@@ -1,5 +0,0 @@
package request
type WafSensitiveDetailReq struct {
Id string `json:"id" form:"id"` //唯一键
}

View File

@@ -1,8 +0,0 @@
package request
type WafSensitiveEditReq struct {
Id string `json:"id"`
Type int `json:"type" form:"type"` //敏感词类型
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
}

View File

@@ -0,0 +1,29 @@
package request
import "SamWaf/model/common/request"
type WafSensitiveAddReq struct {
CheckDirection string `json:"check_direction"` //敏感词检测方向 in ,out ,all
Action string `json:"action"` //敏感词检测后动作 deny,replace
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
}
type WafSensitiveDelReq struct {
Id string `json:"id" form:"id"` //唯一键
}
type WafSensitiveDetailReq struct {
Id string `json:"id" form:"id"` //唯一键
}
type WafSensitiveEditReq struct {
Id string `json:"id"`
CheckDirection string `json:"check_direction"` //敏感词检测方向 in ,out ,all
Action string `json:"action"` //敏感词检测后动作 deny,replace
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
}
type WafSensitiveSearchReq struct {
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
request.PageInfo
}

View File

@@ -1,10 +0,0 @@
package request
import "SamWaf/model/common/request"
type WafSensitiveSearchReq struct {
Type int `json:"type" form:"type"` //敏感词类型
Content string `json:"content" form:"content" ` //内容
Remarks string `json:"remarks" form:"remarks" ` //备注
request.PageInfo
}

View File

@@ -4,7 +4,8 @@ import "SamWaf/model/baseorm"
type Sensitive struct {
baseorm.BaseOrm
Type int `json:"type"` //敏感词类型
Content string `json:"content"` //内容
Remarks string `json:"remarks"` //备注
CheckDirection string `json:"check_direction"` //敏感词检测方向 in ,out ,all
Action string `json:"action"` //敏感词检测后动作 deny,replace
Content string `json:"content"` //内容
Remarks string `json:"remarks"` //备注
}

View File

@@ -24,30 +24,32 @@ func (receiver *WafSensitiveService) AddApi(req request.WafSensitiveAddReq) erro
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
Type: req.Type,
Content: req.Content,
Remarks: req.Remarks,
CheckDirection: req.CheckDirection,
Action: req.Action,
Content: req.Content,
Remarks: req.Remarks,
}
global.GWAF_LOCAL_DB.Create(bean)
return nil
}
func (receiver *WafSensitiveService) CheckIsExistApi(req request.WafSensitiveAddReq) error {
return global.GWAF_LOCAL_DB.First(&model.Sensitive{}, "type = ? and content= ?", req.Type,
return global.GWAF_LOCAL_DB.First(&model.Sensitive{}, "content= ?",
req.Content).Error
}
func (receiver *WafSensitiveService) ModifyApi(req request.WafSensitiveEditReq) error {
var bean model.Sensitive
global.GWAF_LOCAL_DB.Where("type = ? and content= ?", req.Type,
global.GWAF_LOCAL_DB.Where("and content= ?",
req.Content).Find(&bean)
if bean.Id != "" && bean.Type != req.Type && bean.Content != req.Content {
if bean.Id != "" && bean.Content != req.Content {
return errors.New("当前敏感词已经存在")
}
beanMap := map[string]interface{}{
"Type": req.Type,
"Content": req.Content,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
"CheckDirection": req.CheckDirection,
"Action": req.Action,
"Content": req.Content,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
err := global.GWAF_LOCAL_DB.Model(model.Sensitive{}).Where("id = ?", req.Id).Updates(beanMap).Error
@@ -71,12 +73,7 @@ func (receiver *WafSensitiveService) GetListApi(req request.WafSensitiveSearchRe
var whereValues []interface{}
//where字段
whereField = ""
if req.Type > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " type =? "
}
if len(req.Content) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
@@ -90,9 +87,6 @@ func (receiver *WafSensitiveService) GetListApi(req request.WafSensitiveSearchRe
whereField = whereField + " remarks like ? "
}
//where字段赋值
if req.Type > 0 {
whereValues = append(whereValues, req.Type)
}
if len(req.Content) > 0 {
whereValues = append(whereValues, "%"+req.Content+"%")
}

16
utils/http_check.go Normal file
View File

@@ -0,0 +1,16 @@
package utils
import "strings"
// IsContent 是否是内容
func IsContent(contentType string) bool {
var allowedSortFields = []string{"application/json", "text/xml", "application/xml",
"text/plain", "text/html", "text/csv", "application/html"}
for _, allowedField := range allowedSortFields {
if strings.Contains(contentType, allowedField) {
return true
}
}
return false
}

View File

@@ -119,4 +119,11 @@ func pathCoreSql(db *gorm.DB) {
} else {
zlog.Info("db", "hosts :response_time_out init successfully")
}
//20250219 初始化敏感词拦截方式和拦截动作
err = db.Exec("UPDATE sensitives SET check_direction='in',action='deny' WHERE check_direction IS NULL ").Error
if err != nil {
panic("failed to sensitives :response_time_out " + err.Error())
} else {
zlog.Info("db", "sensitives :response_time_out init successfully")
}
}

View File

@@ -1,7 +1,9 @@
package wafenginecore
import (
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/detection"
"SamWaf/model/wafenginmodel"
"net/http"
@@ -22,29 +24,47 @@ func (waf *WafEngine) CheckSensitive(r *http.Request, weblogbean *innerbean.WebL
if len(waf.Sensitive) == 0 {
return result
}
if !waf.CheckRequestSensitive() {
return result
}
//敏感词检测
matchURLResult := waf.SensitiveManager.MultiPatternSearch([]rune(weblogbean.URL), true)
if len(matchURLResult) > 0 {
sensitive := matchURLResult[0].CustomData.(model.Sensitive)
if sensitive.CheckDirection == "out" {
return result
}
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = "敏感词检测:" + string(matchURLResult[0].Word)
result.Content = "敏感词内容"
if sensitive.Action == "deny" {
result.IsBlock = true
result.Title = "敏感词检测:" + string(matchURLResult[0].Word)
result.Content = "敏感词内容"
} else {
result.IsBlock = false
weblogbean.GUEST_IDENTIFICATION = "触发敏感词"
weblogbean.RULE = "敏感词检测:" + string(matchURLResult[0].Word)
waf.ReplaceURLContent(r, string(matchURLResult[0].Word), global.GWAF_HTTP_SENSITIVE_REPLACE_STRING)
}
return result
}
matchBodyResult := waf.SensitiveManager.MultiPatternSearch([]rune(weblogbean.BODY), true)
if len(matchBodyResult) > 0 {
sensitive := matchBodyResult[0].CustomData.(model.Sensitive)
if sensitive.CheckDirection == "out" {
return result
}
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = "敏感词检测:" + string(matchBodyResult[0].Word)
result.Content = "敏感词内容"
return result
}
matchPostFromResult := waf.SensitiveManager.MultiPatternSearch([]rune(weblogbean.POST_FORM), true)
if len(matchPostFromResult) > 0 {
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = "敏感词检测:" + string(matchPostFromResult[0].Word)
result.Content = "敏感词内容"
if sensitive.Action == "deny" {
result.IsBlock = true
result.Title = "敏感词检测:" + string(matchBodyResult[0].Word)
result.Content = "敏感词内容"
} else {
result.IsBlock = false
weblogbean.GUEST_IDENTIFICATION = "触发敏感词"
weblogbean.RULE = "敏感词检测:" + string(matchBodyResult[0].Word)
waf.ReplaceBodyContent(r, string(matchBodyResult[0].Word), global.GWAF_HTTP_SENSITIVE_REPLACE_STRING)
}
return result
}
return result

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"go.uber.org/zap"
"io"
"net/http"
"strconv"
"text/template"
@@ -133,3 +134,107 @@ func EchoErrorInfo(w http.ResponseWriter, r *http.Request, weblogbean innerbean.
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) {
resBytes := []byte("")
var responseCode int = 403
renderData := map[string]interface{}{
"SAMWAF_REQ_UUID": weblogbean.REQ_UUID,
"SAMWAF_BLOCK_INFO": blockInfo,
}
// 处理 hostsafe 的模板
if blockingPage, ok := hostsafe.BlockingPage["other_block"]; ok {
// 设置 HTTP header
var headers []map[string]string
if err := json.Unmarshal([]byte(blockingPage.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(blockingPage.ResponseContent, renderData)
if err == nil {
resBytes = renderedBytes
} else {
resBytes = []byte(blockingPage.ResponseContent)
}
// 设置响应码
if code, err := strconv.Atoi(blockingPage.ResponseCode); err == nil {
responseCode = code
}
} else if globalBlockingPage, ok := globalHostSafe.BlockingPage["other_block"]; 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
}
} else {
// 默认的阻止页面
renderedBytes, err := renderTemplate(global.GLOBAL_DEFAULT_BLOCK_INFO, renderData)
if err == nil {
resBytes = renderedBytes
} else {
resBytes = []byte(global.GLOBAL_DEFAULT_BLOCK_INFO)
}
}
resp.StatusCode = responseCode
resp.Body = io.NopCloser(bytes.NewBuffer(resBytes))
// head 修改追加内容
resp.ContentLength = int64(len(resBytes))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(resBytes)), 10))
resp.Header.Set("Content-Type", "text/html;")
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
// 记录响应body
weblogbean.RES_BODY = string(resBytes)
weblogbean.RULE = ruleName
weblogbean.ACTION = "阻止"
weblogbean.STATUS = "阻止访问"
weblogbean.STATUS_CODE = 403
weblogbean.TASK_FLAG = 1
weblogbean.GUEST_IDENTIFICATION = "可疑用户"
global.GQEQUE_LOG_DB.Enqueue(weblogbean)
}
}

View File

@@ -0,0 +1,90 @@
package wafenginecore
import (
"SamWaf/wafenginecore/wafhttpcore"
"bytes"
"io"
"net/http"
"net/url"
"strings"
)
// 替换Body字符串内容
func (waf *WafEngine) ReplaceBodyContent(r *http.Request, oldString string, newString string) error {
var bodyByte []byte
bodyByte, _ = io.ReadAll(r.Body)
bodyByte = bytes.ReplaceAll(bodyByte, []byte(oldString), []byte(newString))
r.Body = io.NopCloser(bytes.NewBuffer(bodyByte))
return nil
}
// 替换URL字符串内容
func (waf *WafEngine) ReplaceURLContent(r *http.Request, oldString string, newString string) error {
// 深拷贝原始URL
originalURL := r.URL
newURL, _ := url.Parse(originalURL.String())
// 处理路径部分
decodedPath, err := url.PathUnescape(newURL.Path)
if err == nil {
modifiedPath := strings.ReplaceAll(decodedPath, oldString, newString)
if decodedPath != modifiedPath {
newURL.Path = modifiedPath
} else {
newURL.Path = originalURL.Path
}
}
rawQuery := newURL.RawQuery
if rawQuery != "" {
var queryParts []string
// 拆分保留原始顺序
pairs := strings.Split(rawQuery, "&")
for _, pair := range pairs {
// 分割键值对(考虑空值和包含多个=的情况)
key, value, _ := strings.Cut(pair, "=")
originalKey := key
// 处理参数值
if value != "" {
// 保存原始编码值
originalEncoded := value
// 递归解码
decodedValue := wafhttpcore.WafHttpCoreUrlEncode(value, 5)
// 执行替换
modifiedValue := strings.ReplaceAll(decodedValue, oldString, newString)
// 判断是否需要重新编码
if modifiedValue != decodedValue {
value = url.QueryEscape(modifiedValue)
} else {
value = originalEncoded // 保留原始编码
}
}
// 重新构建键值对
if value == "" {
queryParts = append(queryParts, key)
} else {
queryParts = append(queryParts, originalKey+"="+value)
}
}
// 重新组合查询字符串
newURL.RawQuery = strings.Join(queryParts, "&")
} else {
newURL.RawQuery = ""
}
*r.URL = *newURL
r.RequestURI = newURL.RequestURI()
return nil
}
// 替换Form字符串内容
func (waf *WafEngine) ReplaceFormContent(r *http.Request, oldString string, newString string) error {
return nil
}

View File

@@ -24,7 +24,7 @@ import (
"encoding/json"
"errors"
"fmt"
goahocorasick "github.com/anknown/ahocorasick"
goahocorasick "github.com/samwafgo/ahocorasick"
"github.com/satori/go.uuid"
"go.uber.org/zap"
"golang.org/x/net/html/charset"
@@ -218,6 +218,7 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
SrcByteBody: bodyByte,
WebLogVersion: global.GWEBLOG_VERSION,
Scheme: r.Proto,
SrcURL: []byte(r.RequestURI),
}
formValues := url.Values{}
@@ -449,6 +450,7 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
NetSrcIp: utils.GetSourceClientIP(r.RemoteAddr),
WebLogVersion: global.GWEBLOG_VERSION,
Scheme: r.Proto,
SrcURL: []byte(r.RequestURI),
}
//记录响应body
@@ -520,7 +522,6 @@ func (waf *WafEngine) errorResponse() func(http.ResponseWriter, *http.Request, e
func (waf *WafEngine) modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
resp.Header.Set("X-Xss-Protection", "1; mode=block")
r := resp.Request
if wafHttpContext, ok := r.Context().Value("waf_context").(innerbean.WafHttpContextData); ok {
weblogfrist := wafHttpContext.Weblog
@@ -581,8 +582,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
contentType = "img"
}
//文本资源
if respContentType == "text/html" || respContentType == "application/html" ||
respContentType == "application/json" || respContentType == "application/xml" || respContentType == "text/plain" {
if utils.IsContent(respContentType) {
contentType = "text"
}
//记录静态日志
@@ -602,25 +602,51 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
isText = true
break
default:
/*weblogbean.ACTION = "放行"
global.GQEQUE_LOG_DB.PushBack(weblogbean)*/
}
//记录响应body
if isText && resp.Body != nil && resp.Body != http.NoBody && global.GCONFIG_RECORD_RESP == 1 {
if isText && resp.Body != nil && resp.Body != http.NoBody {
//编码转换,自动检测网页编码
//编码转换,自动检测网页编码 resp *http.Response
orgContentBytes, _ := waf.getOrgContent(resp)
//处理敏感词
if waf.CheckResponseSensitive() {
//敏感词检测
matchBodyResult := waf.SensitiveManager.MultiPatternSearch([]rune(string(orgContentBytes)), true)
if len(matchBodyResult) > 0 {
sensitive := matchBodyResult[0].CustomData.(model.Sensitive)
if sensitive.CheckDirection != "in" {
weblogfrist.RISK_LEVEL = 1
if sensitive.Action == "deny" {
EchoResponseErrorInfo(resp, *weblogfrist, "敏感词检测:"+string(matchBodyResult[0].Word), "敏感词内容", waf.HostTarget[host], waf.HostTarget[waf.HostCode[global.GWAF_GLOBAL_HOST_CODE]], true)
return nil
} else {
weblogfrist.GUEST_IDENTIFICATION = "触发敏感词"
weblogfrist.RULE = "敏感词检测:" + string(matchBodyResult[0].Word)
orgContentBytes = bytes.ReplaceAll(orgContentBytes, []byte(string(matchBodyResult[0].Word)), []byte(global.GWAF_HTTP_SENSITIVE_REPLACE_STRING))
}
}
}
}
finalCompressBytes, _ := waf.compressContent(resp, orgContentBytes)
resp.Body = io.NopCloser(bytes.NewBuffer(finalCompressBytes))
// head 修改追加内容
resp.ContentLength = int64(len(finalCompressBytes))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(finalCompressBytes)), 10))
if resp.ContentLength < global.GCONFIG_RECORD_MAX_RES_BODY_LENGTH {
weblogfrist.RES_BODY = string(orgContentBytes)
weblogfrist.SrcByteResBody = orgContentBytes
if global.GCONFIG_RECORD_RESP == 1 {
if resp.ContentLength < global.GCONFIG_RECORD_MAX_RES_BODY_LENGTH {
weblogfrist.RES_BODY = string(orgContentBytes)
weblogfrist.SrcByteResBody = orgContentBytes
}
}
}
zlog.Debug("TESTChanllage", weblogfrist.HOST, weblogfrist.URL)
if !isStaticAssist {

View File

@@ -0,0 +1,144 @@
package wafenginecore
import (
"SamWaf/common/zlog"
"SamWaf/global"
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
)
func TestReplaceBodyContent(t *testing.T) {
// 测试用例表格
tests := []struct {
name string
inputBody string
oldString string
newString string
wantBody string
wantError bool
contentType string
}{
{
name: "basic replacement",
inputBody: "Hello world",
oldString: "world",
newString: "golang",
wantBody: "Hello golang",
},
{
name: "multiple occurrences",
inputBody: "banana",
oldString: "na",
newString: "no",
wantBody: "banono",
},
{
name: "empty body",
inputBody: "",
oldString: "test",
newString: "demo",
wantBody: "",
},
{
name: "chinese characters",
inputBody: "你好世界",
oldString: "世界",
newString: "Golang",
wantBody: "你好Golang",
},
{
name: "binary data",
inputBody: string([]byte{0x48, 0x65, 0x00, 0x6c, 0x6c, 0x6f}), // 包含null字节
oldString: string([]byte{0x00}),
newString: " ",
wantBody: "He llo",
},
{
name: "case sensitive",
inputBody: "Go is Cool, go is fun",
oldString: "go",
newString: "GO",
wantBody: "Go is Cool, GO is fun",
},
{
name: "json content",
contentType: "application/json",
inputBody: `{"message":"hello"}`,
oldString: "hello",
newString: "hola",
wantBody: `{"message":"hola"}`,
},
}
waf := &WafEngine{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建测试请求
req := httptest.NewRequest("POST", "/", bytes.NewBufferString(tt.inputBody))
if tt.contentType != "" {
req.Header.Set("Content-Type", tt.contentType)
}
// 执行替换操作
err := waf.ReplaceBodyContent(req, tt.oldString, tt.newString)
if (err != nil) != tt.wantError {
t.Fatalf("ReplaceBodyContent() error = %v, wantError %v", err, tt.wantError)
}
// 读取修改后的Body
gotBody, err := io.ReadAll(req.Body)
if err != nil {
t.Fatal("Failed to read modified body:", err)
}
// 验证结果
if string(gotBody) != tt.wantBody {
t.Errorf("Body mismatch\nWant: %q\nGot: %q", tt.wantBody, string(gotBody))
}
// 验证Content-Type是否保留
if ct := req.Header.Get("Content-Type"); ct != tt.contentType {
t.Errorf("Content-Type changed unexpectedly\nWas: %q\nNow: %q", tt.contentType, ct)
}
})
}
}
func TestReplaceURLContent(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_RELEASE)
if v := recover(); v != nil {
zlog.Error("error")
}
// 模拟原始请求
req := httptest.NewRequest("GET", "http://origin.com/test%20/scan?q=hello%2520world&n=1%2B1", nil)
waf := &WafEngine{}
// 执行替换:将"test "替换为"demo"
err := waf.ReplaceURLContent(req, "test ", "demo")
if err != nil {
t.Fatal(err)
}
// 验证路径编码
if req.URL.Path != "/demo/scan" {
t.Errorf("RawPath mismatch: %s", req.URL.RawPath)
}
// 模拟代理服务器接收的请求
proxyReq := &http.Request{
Method: req.Method,
URL: req.URL,
Header: req.Header,
}
// 验证代理看到的URL
if proxyReq.URL.String() != req.URL.String() {
t.Errorf("Proxy URL mismatch: %s", proxyReq.URL.String())
}
}

View File

@@ -13,7 +13,7 @@ import (
"SamWaf/webplugin"
"context"
"encoding/base64"
goahocorasick "github.com/anknown/ahocorasick"
goahocorasick "github.com/samwafgo/ahocorasick"
"golang.org/x/time/rate"
"net/http"
"strconv"
@@ -310,13 +310,15 @@ func (waf *WafEngine) ReLoadSensitive() {
}
// 提取 content 字段并转换为 [][]rune
var keywords [][]rune
var customData map[string]interface{}
customData = make(map[string]interface{})
for _, sensitive := range waf.Sensitive {
// 将 content 转换为 []rune 并追加到 keywords
keywords = append(keywords, []rune(sensitive.Content))
customData[sensitive.Content] = sensitive
}
m := new(goahocorasick.Machine)
err := m.Build(keywords)
err := m.BuildByCustom(keywords, customData)
if err != nil {
zlog.Error("load sensitive error", err)
return
@@ -325,6 +327,30 @@ func (waf *WafEngine) ReLoadSensitive() {
}
// CheckRequestSensitive 检查是否需要请求敏感词检测
func (waf *WafEngine) CheckRequestSensitive() bool {
var bean model.Sensitive
//只要不是检测返回的,那说明是检查请求的
global.GWAF_LOCAL_DB.Where("check_direction!=?", "out").Find(&bean).Limit(1)
if len(bean.Id) > 0 {
return true
} else {
return false
}
}
// CheckResponseSensitive 检查是否需要响应敏感词检测
func (waf *WafEngine) CheckResponseSensitive() bool {
var bean model.Sensitive
//只要不是检测请求的,那说明是检查返回的
global.GWAF_LOCAL_DB.Where("check_direction!=?", "in").Find(&bean).Limit(1)
if len(bean.Id) > 0 {
return true
} else {
return false
}
}
// DoHttpAuthBase Http auth base 检测
func (waf *WafEngine) DoHttpAuthBase(hostSafe *wafenginmodel.HostSafe, w http.ResponseWriter, r *http.Request) (bool, string) {
isStop := false