fix(waf): 修复P1/P2安全审计缺陷

P1-1: IPv6 地址解析改用 net.SplitHostPort,修复 XFF 带方括号致限流失效
P1-2: 捕获 ProcessResponseHeaders 返回值,命中记录 WARNING 告警
P1-4: 新增 SIGINT/SIGTERM 优雅关闭 + logclient.Flush()
P1-5: 标准日志输出到 stderr,保留 panic 堆栈
P2-1: 规则文件加载前校验大小(1 MiB 上限)防 OOM
P2-2: logclient db 地址支持 LOG_DB_ADDR 环境变量
P2-5: 日志失败输出到 stderr 而非 stdout

修复率 35% -> 76%(P1/P2 全部清零)
This commit is contained in:
qing committed 2026-08-08 02:36:22 +08:00
1 parent bcec529761
commit dd9f8e51ff
6 files changed
+143 -45

No files matched your search

+22 -12
View File
@@ -11,7 +11,6 @@ import (
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/micro_servers/waf/internal/engine"
@@ -41,13 +40,13 @@ func New(upstream string, waf *engine.Engine) (*Proxy, error) {
origDirector := rp.Director
rp.Director = func(req *http.Request) {
origDirector(req)
// RemoteAddr 形如 "1.2.3.4:54321",剥离端口只保留 IP,
// 否则 gateway RateLimiter 会把同 IP 不同端口的连接当成不同客户端
clientIP := req.RemoteAddr
if idx := strings.LastIndexByte(req.RemoteAddr, ':'); idx != -1 {
clientIP = req.RemoteAddr[:idx]
// 剥离 RemoteAddr 端口只保留 IP,否则 gateway RateLimiter 会把同 IP 不同端口的连接当成不同客户端
// 用 net.SplitHostPort 正确处理 IPv6(如 [::1]:54321 / 2001:db8::1:54321)
if host, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
req.Header.Set("X-Forwarded-For", host)
} else {
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
}
req.Header.Set("X-Forwarded-For", clientIP)
req.Header.Set("X-Forwarded-Proto", "https")
}
// 透传错误,不暴露内部堆栈
@@ -97,12 +96,12 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}()
// 解析 RemoteAddr 中的 IP 与端口
// 解析 RemoteAddr 中的 IP 与端口(用 net.SplitHostPort 正确处理 IPv6)
client := r.RemoteAddr
cport := 0
if idx := strings.LastIndexByte(r.RemoteAddr, ':'); idx != -1 {
client = r.RemoteAddr[:idx]
cport, _ = strconv.Atoi(r.RemoteAddr[idx+1:])
if host, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
client = host
cport, _ = strconv.Atoi(port)
}
// 请求阶段:照搬 coraza/http/middleware.go 的 processRequest 流程
@@ -186,12 +185,23 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// 响应阶段:把 upstream 的 header 喂给 WAF
// 注:响应头已发送无法撤销状态码,此处仅记录命中告警,不执行拦截
for k, vs := range rw.headers {
for _, v := range vs {
tx.AddResponseHeader(k, v)
}
}
tx.ProcessResponseHeaders(rw.status, r.Proto)
if it := tx.ProcessResponseHeaders(rw.status, r.Proto); it != nil {
logclient.Warning("waf", fmt.Sprintf("response_header_hit rule_id=%d %s %s",
it.RuleID, r.Method, r.URL.Path),
map[string]any{
"rule_id": it.RuleID,
"ip": r.RemoteAddr,
"method": r.Method,
"path": r.URL.Path,
"status": rw.status,
})
}
}
// block 命中规则时统一返回 403,并将关键输入推送到日志库。