fix:weblog slow

#782
This commit is contained in:
samwaf
2026-05-26 11:39:45 +08:00
parent f2d0e9e5ed
commit 261d2f3865
8 changed files with 172 additions and 23 deletions
+27 -7
View File
@@ -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)
+58 -4
View File
@@ -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)
}
+17
View File
@@ -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{}{
+1 -1
View File
@@ -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") != "" {
// 处理压缩的请求体
+2
View File
@@ -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
View File
@@ -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)
}
}
+2
View File
@@ -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
View File
@@ -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 规则匹配。
// 推荐值:524288512 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 {