mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-08-31 01:41:39 +08:00
+27
-7
@@ -607,13 +607,16 @@ func (w *WafOwaspApi) TuningSetApi(c *gin.Context) {
|
||||
return ""
|
||||
}
|
||||
req := wafowasp.TuningConfig{
|
||||
BlockingParanoia: pickInt("blocking_paranoia_level", "blocking_paranoia"),
|
||||
DetectionParanoia: pickInt("detection_paranoia_level", "detection_paranoia"),
|
||||
InboundThreshold: pickInt("inbound_anomaly_score_threshold", "inbound_threshold"),
|
||||
OutboundThreshold: pickInt("outbound_anomaly_score_threshold", "outbound_threshold"),
|
||||
RuleEngine: pickStr("rule_engine"),
|
||||
EarlyBlocking: pickInt("early_blocking"),
|
||||
EnforceBodyProcessor: pickInt("enforce_bodyproc_urlencoded", "enforce_body_processor"),
|
||||
BlockingParanoia: pickInt("blocking_paranoia_level", "blocking_paranoia"),
|
||||
DetectionParanoia: pickInt("detection_paranoia_level", "detection_paranoia"),
|
||||
InboundThreshold: pickInt("inbound_anomaly_score_threshold", "inbound_threshold"),
|
||||
OutboundThreshold: pickInt("outbound_anomaly_score_threshold", "outbound_threshold"),
|
||||
RuleEngine: pickStr("rule_engine"),
|
||||
EarlyBlocking: pickInt("early_blocking"),
|
||||
EnforceBodyProcessor: pickInt("enforce_bodyproc_urlencoded", "enforce_body_processor"),
|
||||
RequestBodyLimit: pickInt("request_body_limit"),
|
||||
RequestBodyInMemoryLimit: pickInt("request_body_in_memory_limit"),
|
||||
BodyInspectLimit: pickInt("body_inspect_limit"),
|
||||
}
|
||||
if req.BlockingParanoia < 1 || req.BlockingParanoia > 4 {
|
||||
response.FailWithMessage("blocking_paranoia_level 必须在 1..4", c)
|
||||
@@ -637,6 +640,23 @@ func (w *WafOwaspApi) TuningSetApi(c *gin.Context) {
|
||||
response.FailWithMessage("rule_engine 取值应为 On/DetectionOnly/Off", c)
|
||||
return
|
||||
}
|
||||
if req.RequestBodyLimit < 0 {
|
||||
response.FailWithMessage("request_body_limit 不能为负数", c)
|
||||
return
|
||||
}
|
||||
if req.RequestBodyInMemoryLimit < 0 {
|
||||
response.FailWithMessage("request_body_in_memory_limit 不能为负数", c)
|
||||
return
|
||||
}
|
||||
if req.RequestBodyLimit > 0 && req.RequestBodyInMemoryLimit > 0 &&
|
||||
req.RequestBodyInMemoryLimit > req.RequestBodyLimit {
|
||||
response.FailWithMessage("request_body_in_memory_limit 不能大于 request_body_limit", c)
|
||||
return
|
||||
}
|
||||
if req.BodyInspectLimit < 0 {
|
||||
response.FailWithMessage("body_inspect_limit 不能为负数", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.ApplyTuning(req); err != nil {
|
||||
response.FailWithMessage("应用失败: "+err.Error(), c)
|
||||
|
||||
@@ -11,12 +11,66 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type WafLogService struct{}
|
||||
|
||||
var WafLogServiceApp = new(WafLogService)
|
||||
|
||||
// listExcludeColumns 列表查询排除的列:大文本字段和原始 blob 字段
|
||||
var listExcludeColumns = map[string]bool{
|
||||
"body": true, "res_body": true, "post_form": true,
|
||||
"src_byte_body": true, "src_byte_res_body": true, "src_url": true,
|
||||
}
|
||||
|
||||
// detailExcludeColumns 详情查询排除的列:仅排除原始 blob 字段,保留文本 body 类字段
|
||||
var detailExcludeColumns = map[string]bool{
|
||||
"src_byte_body": true, "src_byte_res_body": true, "src_url": true,
|
||||
}
|
||||
|
||||
var (
|
||||
webLogListSelectOnce sync.Once
|
||||
webLogDetailSelectOnce sync.Once
|
||||
webLogListSelectCache string
|
||||
webLogDetailSelectCache string
|
||||
)
|
||||
|
||||
// getWebLogListSelect 动态从 WebLog 结构体反射出列名并排除大字段,结果缓存复用。
|
||||
// 新增字段会自动纳入,旧版本数据库缺列也不影响(GORM 会忽略不存在的列)。
|
||||
func getWebLogListSelect() string {
|
||||
webLogListSelectOnce.Do(func() {
|
||||
webLogListSelectCache = buildSelectExcluding(&innerbean.WebLog{}, listExcludeColumns)
|
||||
})
|
||||
return webLogListSelectCache
|
||||
}
|
||||
|
||||
// getWebLogDetailSelect 详情查询字段,包含文本 body 类字段,排除 blob。
|
||||
func getWebLogDetailSelect() string {
|
||||
webLogDetailSelectOnce.Do(func() {
|
||||
webLogDetailSelectCache = buildSelectExcluding(&innerbean.WebLog{}, detailExcludeColumns)
|
||||
})
|
||||
return webLogDetailSelectCache
|
||||
}
|
||||
|
||||
// buildSelectExcluding 通过 GORM schema 解析模型字段,返回排除指定列后的 SELECT 子句。
|
||||
func buildSelectExcluding(model interface{}, excludeDBNames map[string]bool) string {
|
||||
s, err := schema.Parse(model, &sync.Map{}, schema.NamingStrategy{})
|
||||
if err != nil {
|
||||
return "*"
|
||||
}
|
||||
cols := make([]string, 0, len(s.Fields))
|
||||
for _, field := range s.Fields {
|
||||
if field.DBName == "" || excludeDBNames[field.DBName] {
|
||||
continue
|
||||
}
|
||||
cols = append(cols, field.DBName)
|
||||
}
|
||||
return strings.Join(cols, ", ")
|
||||
}
|
||||
|
||||
func (receiver *WafLogService) AddApi(log innerbean.WebLog) error {
|
||||
global.GWAF_LOCAL_LOG_DB.Create(log)
|
||||
return nil
|
||||
@@ -27,10 +81,10 @@ func (receiver *WafLogService) ModifyApi(log innerbean.WebLog) error {
|
||||
func (receiver *WafLogService) GetDetailApi(req request.WafAttackLogDetailReq) (innerbean.WebLog, error) {
|
||||
var weblog innerbean.WebLog
|
||||
if len(req.CurrrentDbName) == 0 || req.CurrrentDbName == "local_log.db" {
|
||||
global.GWAF_LOCAL_LOG_DB.Where("REQ_UUID=?", req.REQ_UUID).Find(&weblog)
|
||||
global.GWAF_LOCAL_LOG_DB.Select(getWebLogDetailSelect()).Where("REQ_UUID=?", req.REQ_UUID).Find(&weblog)
|
||||
} else {
|
||||
wafdb.InitManaulLogDb("", req.CurrrentDbName)
|
||||
global.GDATA_CURRENT_LOG_DB_MAP[req.CurrrentDbName].Where("REQ_UUID=?", req.REQ_UUID).Find(&weblog)
|
||||
global.GDATA_CURRENT_LOG_DB_MAP[req.CurrrentDbName].Select(getWebLogDetailSelect()).Where("REQ_UUID=?", req.REQ_UUID).Find(&weblog)
|
||||
}
|
||||
|
||||
return weblog, nil
|
||||
@@ -186,11 +240,11 @@ func (receiver *WafLogService) GetListApi(req request.WafAttackLogSearch) ([]inn
|
||||
return nil, 0, errors.New("输入排序字段不合法")
|
||||
}
|
||||
if len(req.CurrrentDbName) == 0 || req.CurrrentDbName == "local_log.db" {
|
||||
global.GWAF_LOCAL_LOG_DB.Table(forceIndex).Limit(req.PageSize).Where(whereField, whereValues...).Offset(req.PageSize * (req.PageIndex - 1)).Order(orderInfo).Find(&weblogs)
|
||||
global.GWAF_LOCAL_LOG_DB.Select(getWebLogListSelect()).Table(forceIndex).Limit(req.PageSize).Where(whereField, whereValues...).Offset(req.PageSize * (req.PageIndex - 1)).Order(orderInfo).Find(&weblogs)
|
||||
global.GWAF_LOCAL_LOG_DB.Table(forceIndex).Where(whereField, whereValues...).Count(&total)
|
||||
} else {
|
||||
wafdb.InitManaulLogDb("", req.CurrrentDbName)
|
||||
global.GDATA_CURRENT_LOG_DB_MAP[req.CurrrentDbName].Table(forceIndex).Limit(req.PageSize).Where(whereField, whereValues...).Offset(req.PageSize * (req.PageIndex - 1)).Order(orderInfo).Find(&weblogs)
|
||||
global.GDATA_CURRENT_LOG_DB_MAP[req.CurrrentDbName].Select(getWebLogListSelect()).Table(forceIndex).Limit(req.PageSize).Where(whereField, whereValues...).Offset(req.PageSize * (req.PageIndex - 1)).Order(orderInfo).Find(&weblogs)
|
||||
global.GDATA_CURRENT_LOG_DB_MAP[req.CurrrentDbName].Table(forceIndex).Where(whereField, whereValues...).Count(&total)
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckOwasp OWASP CRS 检测。
|
||||
@@ -45,7 +46,23 @@ func (waf *WafEngine) CheckOwasp(r *http.Request, weblogbean *innerbean.WebLog,
|
||||
return result
|
||||
}
|
||||
|
||||
owaspStart := time.Now()
|
||||
isInteeruption, interruption, err := inst.ProcessRequest(r, weblogbean)
|
||||
if elapsed := time.Since(owaspStart); elapsed > 10*time.Second {
|
||||
hint := ""
|
||||
if wafowasp.GetBodyInspectLimit() == 0 {
|
||||
hint = "可在 OWASP 调参中设置 body_inspect_limit(如 524288=512KB)以避免大 body 的正则回溯超时"
|
||||
}
|
||||
zlog.Warn("CheckOwasp slow", map[string]interface{}{
|
||||
"elapsed_ms": elapsed.Milliseconds(),
|
||||
"method": r.Method,
|
||||
"uri": r.URL.RequestURI(),
|
||||
"host": r.Host,
|
||||
"src_ip": weblogbean.SRC_IP,
|
||||
"body_inspect_limit": wafowasp.GetBodyInspectLimit(),
|
||||
"hint": hint,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
// Coraza 处理异常:记录错误但不影响请求(fail-open),避免把引擎故障误判为攻击
|
||||
zlog.Error("CheckOwasp ProcessRequest err", map[string]interface{}{
|
||||
|
||||
@@ -313,7 +313,7 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var bodyByte []byte
|
||||
|
||||
// 拷贝一份request的Body ,控制不记录大文件的情况
|
||||
if r.Body != nil && r.Body != http.NoBody && (contentLength < (global.GCONFIG_RECORD_MAX_BODY_LENGTH) || cacheConfig.IsEnableCache == 1) {
|
||||
if r.Body != nil && r.Body != http.NoBody && (contentLength > 0) && (contentLength < (global.GCONFIG_RECORD_MAX_BODY_LENGTH) || cacheConfig.IsEnableCache == 1) {
|
||||
// 检查请求是否包含Content-Encoding
|
||||
if r.Header.Get("Content-Encoding") != "" {
|
||||
// 处理压缩的请求体
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -250,6 +251,7 @@ func (web *WafWebManager) StartLocalServer() error {
|
||||
}
|
||||
r := gin.Default()
|
||||
r.Use(web.cors()) //解决跨域
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
web.initRouter(r)
|
||||
|
||||
web.R = r
|
||||
|
||||
+42
-4
@@ -65,6 +65,25 @@ func GetEngineMode() string {
|
||||
return "On"
|
||||
}
|
||||
|
||||
// bodyInspectLimit 限制送入 Coraza Phase 2 检测的请求体字节数。
|
||||
// 0 = 无限制(默认,向后兼容);>0 = 最多检测 N 字节。
|
||||
// 超出部分不送入 Coraza,但 r.Body 仍完整还原供下游代理使用。
|
||||
// 由 TuningSetApi → ApplyTuning → SetBodyInspectLimit 同步。
|
||||
var bodyInspectLimit atomic.Int64
|
||||
|
||||
// SetBodyInspectLimit 设置请求体检测字节上限(0 = 无限制)。
|
||||
func SetBodyInspectLimit(n int) {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
bodyInspectLimit.Store(int64(n))
|
||||
}
|
||||
|
||||
// GetBodyInspectLimit 返回当前检测字节上限(0 = 无限制)。
|
||||
func GetBodyInspectLimit() int64 {
|
||||
return bodyInspectLimit.Load()
|
||||
}
|
||||
|
||||
// bodyBufferPool 用于回放 r.Body 时复用底层缓冲区,降低 GC 压力。
|
||||
var bodyBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
@@ -357,8 +376,18 @@ func (w *WafOWASP) processRequestHeaders(tx types.Transaction, r *http.Request)
|
||||
func (w *WafOWASP) processRequestBody(tx types.Transaction, r *http.Request, weblog *innerbean.WebLog) error {
|
||||
// 优先使用上游已经读取好的 body,避免二次读 r.Body
|
||||
if weblog != nil && weblog.BODY != "" {
|
||||
limit := GetBodyInspectLimit()
|
||||
bodyLen := int64(len(weblog.BODY))
|
||||
// strings.NewReader 与底层字符串共享只读数据,零拷贝
|
||||
if _, _, err := tx.ReadRequestBodyFrom(strings.NewReader(weblog.BODY)); err != nil {
|
||||
var reader io.Reader = strings.NewReader(weblog.BODY)
|
||||
if limit > 0 && bodyLen > limit {
|
||||
reader = io.LimitReader(reader, limit)
|
||||
zlog.Warn("processRequestBody: body truncated for Coraza inspection", map[string]interface{}{
|
||||
"original_bytes": bodyLen,
|
||||
"inspect_limit": limit,
|
||||
})
|
||||
}
|
||||
if _, _, err := tx.ReadRequestBodyFrom(reader); err != nil {
|
||||
return fmt.Errorf("failed to feed weblog body to coraza: %v", err)
|
||||
}
|
||||
return nil
|
||||
@@ -385,13 +414,22 @@ func (w *WafOWASP) processRequestBody(tx types.Transaction, r *http.Request, web
|
||||
}
|
||||
|
||||
data := buf.Bytes()
|
||||
// 重置 body 以便下游继续消费
|
||||
// 重置 body 以便下游继续消费(完整副本,不受检测截断影响)
|
||||
bodyCopy := make([]byte, len(data))
|
||||
copy(bodyCopy, data)
|
||||
r.Body = io.NopCloser(bytes.NewReader(bodyCopy))
|
||||
|
||||
if len(bodyCopy) > 0 {
|
||||
if _, _, err := tx.WriteRequestBody(bodyCopy); err != nil {
|
||||
// 仅截断送入 Coraza 的部分,r.Body 已还原为完整数据
|
||||
inspectData := bodyCopy
|
||||
if limit := GetBodyInspectLimit(); limit > 0 && int64(len(bodyCopy)) > limit {
|
||||
inspectData = bodyCopy[:limit]
|
||||
zlog.Warn("processRequestBody: body truncated for Coraza inspection", map[string]interface{}{
|
||||
"original_bytes": len(bodyCopy),
|
||||
"inspect_limit": limit,
|
||||
})
|
||||
}
|
||||
if len(inspectData) > 0 {
|
||||
if _, _, err := tx.WriteRequestBody(inspectData); err != nil {
|
||||
return fmt.Errorf("failed to write request body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func NewOwaspManager(currentDir string) *OwaspManager {
|
||||
// 热路径据此决定 DetectionOnly 下是否记 INFO 日志
|
||||
if t, err := m.overrides.GetTuning(); err == nil {
|
||||
SetEngineMode(t.RuleEngine)
|
||||
SetBodyInspectLimit(t.BodyInspectLimit) // 从磁盘恢复检测字节上限
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -76,6 +77,7 @@ func (m *OwaspManager) ApplyTuning(t TuningConfig) error {
|
||||
}
|
||||
// 同步引擎模式,供热路径判定"DetectionOnly 本该拦截"时记 INFO 日志
|
||||
SetEngineMode(t.RuleEngine)
|
||||
SetBodyInspectLimit(t.BodyInspectLimit) // 运行时热更新检测字节上限
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+23
-7
@@ -140,13 +140,20 @@ type RuleOverrideEntry struct {
|
||||
|
||||
// TuningConfig 全局调优参数。
|
||||
type TuningConfig struct {
|
||||
BlockingParanoia int `json:"blocking_paranoia_level"` // 1..4
|
||||
DetectionParanoia int `json:"detection_paranoia_level"` // >= blocking
|
||||
InboundThreshold int `json:"inbound_anomaly_score_threshold"`
|
||||
OutboundThreshold int `json:"outbound_anomaly_score_threshold"`
|
||||
RuleEngine string `json:"rule_engine"` // On / DetectionOnly / Off
|
||||
EarlyBlocking int `json:"early_blocking"` // 0/1
|
||||
EnforceBodyProcessor int `json:"enforce_bodyproc_urlencoded"` // 0/1
|
||||
BlockingParanoia int `json:"blocking_paranoia_level"` // 1..4
|
||||
DetectionParanoia int `json:"detection_paranoia_level"` // >= blocking
|
||||
InboundThreshold int `json:"inbound_anomaly_score_threshold"`
|
||||
OutboundThreshold int `json:"outbound_anomaly_score_threshold"`
|
||||
RuleEngine string `json:"rule_engine"` // On / DetectionOnly / Off
|
||||
EarlyBlocking int `json:"early_blocking"` // 0/1
|
||||
EnforceBodyProcessor int `json:"enforce_bodyproc_urlencoded"` // 0/1
|
||||
RequestBodyLimit int `json:"request_body_limit"` // 0=不覆盖,>0 字节数(对应 SecRequestBodyLimit)
|
||||
RequestBodyInMemoryLimit int `json:"request_body_in_memory_limit"` // 0=不覆盖,>0 字节数(对应 SecRequestBodyInMemoryLimit)
|
||||
// BodyInspectLimit 送入 Coraza 规则检测的最大字节数(纯 Go 层截断,不写入 coraza.conf)。
|
||||
// 0 = 无限制(默认,向后兼容);>0 = 最多检测 N 字节。
|
||||
// r.Body 始终完整还原供下游代理,截断仅影响 Coraza 规则匹配。
|
||||
// 推荐值:524288(512 KB)可大幅减少大 base64 body 的正则回溯超时。
|
||||
BodyInspectLimit int `json:"body_inspect_limit"`
|
||||
// CustomVars 用户自定义 CRS 事务变量(如 tx.allowed_methods)。
|
||||
// key 不含 tx. 前缀(如 "allowed_methods"),value 为字符串值。
|
||||
// 写入 00-tuning.conf 时以 SecAction setvar:'tx.KEY=VALUE' 形式追加。
|
||||
@@ -837,6 +844,15 @@ func writeTuningConfFile(path string, t TuningConfig) error {
|
||||
writeSetvar(950006, "Enforce Body Processor URLENCODED", "enforce_bodyproc_urlencoded", 1)
|
||||
}
|
||||
|
||||
// SecRequestBodyLimit / SecRequestBodyInMemoryLimit 是 Coraza 引擎指令,不能用 setvar。
|
||||
// 0 = 不写出,保留 coraza.conf 中的默认值(13 MB / 128 KB)。
|
||||
if t.RequestBodyLimit > 0 {
|
||||
sb.WriteString(fmt.Sprintf("# Request body size limit (bytes)\nSecRequestBodyLimit %d\n\n", t.RequestBodyLimit))
|
||||
}
|
||||
if t.RequestBodyInMemoryLimit > 0 {
|
||||
sb.WriteString(fmt.Sprintf("# Request body in-memory limit (bytes)\nSecRequestBodyInMemoryLimit %d\n\n", t.RequestBodyInMemoryLimit))
|
||||
}
|
||||
|
||||
// 用户自定义 CRS 事务变量(tx.allowed_methods 等)
|
||||
// 按 key 排序保证文件内容稳定,从 ID 950100 起步
|
||||
if len(t.CustomVars) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user