mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2025-12-06 06:58:54 +08:00
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
3
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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 方法中定义复合索引
|
||||
|
||||
@@ -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" ` //备注
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package request
|
||||
|
||||
type WafSensitiveDelReq struct {
|
||||
Id string `json:"id" form:"id"` //唯一键
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package request
|
||||
|
||||
type WafSensitiveDetailReq struct {
|
||||
Id string `json:"id" form:"id"` //唯一键
|
||||
}
|
||||
@@ -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" ` //备注
|
||||
}
|
||||
29
model/request/waf_sensitive_req.go
Normal file
29
model/request/waf_sensitive_req.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"` //备注
|
||||
}
|
||||
|
||||
@@ -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
16
utils/http_check.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
90
wafenginecore/replace_request.go
Normal file
90
wafenginecore/replace_request.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
144
wafenginecore/wafengine_test.go
Normal file
144
wafenginecore/wafengine_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user