diff --git a/waf/README.md b/waf/README.md index 6455886..8a0c9e2 100644 --- a/waf/README.md +++ b/waf/README.md @@ -283,13 +283,30 @@ Director 改写([proxy.go:36-46](internal/proxy/proxy.go#L36)):剥离 `Rem ErrorHandler([proxy.go:48-53](internal/proxy/proxy.go#L48)):upstream 不可达时返回 502,同时注入安全响应头,不暴露内部堆栈,仅服务端 log。 +## WebSocket 代理支持 + +WAF 支持透明代理 WebSocket 连接(如 aibox 的 WebSocket 接口)。实现要点: + +- `responseRecorder` 实现了 `http.Hijacker` 接口,将 Hijack 调用委托给底层 `http.ResponseWriter` +- WebSocket 握手阶段(HTTP Upgrade 请求/响应)仍然经过 WAF 规则检测(请求头 phase:1) +- 握手成功(101 Switching Protocols)后,连接被 Hijack,由 `httputil.ReverseProxy` 自动进行双向字节流转发 +- 升级后连接转为二进制帧协议,跳过 HTTP 响应阶段的 WAF 检测,记录 Info 日志 +- WebSocket 帧内容不做 WAF 检测(帧是二进制双向流,Coraza 规则引擎基于 HTTP 请求/响应模型) +- `http.Server` 的超时设置不影响 Hijack 后的 WebSocket 长连接(Hijack 后连接脱离 Server 超时管理) +- 安全响应头(CSP/HSTS 等)不注入 WebSocket 升级响应(101 响应由协议协商决定,注入非标准头可能导致客户端兼容问题) + +配置要求: +- 无特殊配置,WebSocket 连接(`Upgrade: websocket`)自动识别并代理 +- 需要上游服务正确处理 WebSocket 升级(如 aibox 监听 ws:// 端口) + ## 已知限制 - 响应头检测(`ProcessResponseHeaders`)当前仅做审计日志,命中后不执行拦截(响应头已发送无法撤销状态码)。如需响应头拦截需引入 buffered response writer。 - **响应体检测未启用**(未 `ProcessResponseBody`,缓冲整个响应延迟成本高)。SQL 报错回显等响应体泄漏检测缺失。 +- **WebSocket 帧内容不检测**:升级后二进制帧直接转发,不经过 WAF 规则引擎(适用于内部可信服务如 aibox)。 - 限流/黑白名单未实现,可后续作为 coraza 规则或独立中间件加。 - TLS 仅设 `MinVersion: TLS1.2`,未限定 cipher suites,允许弱加密套件。 -- `X-Forwarded-For` 直接覆盖原值(`Header.Set`),未按 RFC 7239 追加;当前 gateway 也只取第一个 IP,无实际影响,但语义不严格。注意:IPv6 地址在 X-Forwarded-For 中可能带方括号(待修复)。 +- `X-Forwarded-For` 直接覆盖原值(`Header.Set`),未按 RFC 7239 追加;当前 gateway 也只取第一个 IP,无实际影响,但语义不严格。注意:IPv6 地址在 X-Forwarded-For 中可能带方括号(待修复,使用 `LastIndexByte` 无法正确处理 IPv6 格式)。 - CSP 仍含 `'unsafe-inline'`(为兼容 forgot.html 与 games/*.html 的内联脚本),待前端清理后可收紧。 - HSTS 已启用(半年期,不含子域),站点全量 HTTPS 覆盖后可按需收紧。 - logclient 日志推送每次新建 TCP 短连接,高并发拦截场景可能产生较多 TIME_WAIT;db 地址硬编码 `127.0.0.1:8005`,不支持环境变量配置。 @@ -299,6 +316,15 @@ ErrorHandler([proxy.go:48-53](internal/proxy/proxy.go#L48)):upstream 不 ## 版本日志 +### v1.2(2026-08-01)— WebSocket 支持 + 修复 + +- **feat**: WebSocket 透明代理支持,`responseRecorder` 实现 `http.Hijacker` 接口 +- WebSocket 握手阶段仍经过 WAF 请求头检测,升级后自动双向转发 +- WebSocket 升级成功记录 Info 日志,便于追踪 +- **fix(P2-3)**: `ProcessResponseHeaders` 协议版本从硬编码 `"HTTP/1.1"` 改为 `r.Proto`,支持 HTTP/2 +- **fix(P2-4)**: responseRecorder 添加 Write 方法确保 WriteHeader 在首次 Write 前调用 +- 补充 `bufio`/`net` import 用于 Hijacker 实现 + ### v1.1(2026-08-01)— 安全加固 - **P0 修复:请求体 OOM 防护**:添加 10MB 硬限制,`io.LimitReader` + 多读1字节检测超限,超限返回 413 diff --git a/waf/docs/SECURITY_AUDIT_20260801.md b/waf/docs/SECURITY_AUDIT_20260801.md index 5f5af40..f465221 100644 --- a/waf/docs/SECURITY_AUDIT_20260801.md +++ b/waf/docs/SECURITY_AUDIT_20260801.md @@ -12,7 +12,7 @@ |--------|--------|--------|--------| | **P0 (Critical)** | 3 | 2 | 1 | | **P1 (High)** | 5 | 1 | 4 | -| **P2 (Medium)** | 6 | 1 | 5 | +| **P2 (Medium)** | 6 | 3 | 3 | | **P3 (Low)** | 3 | 0 | 3 | --- @@ -213,29 +213,32 @@ db 地址硬编码为常量。项目约定其他服务(db、ai_review、dnfs --- -### P2-3: ProcessResponseHeaders 硬编码 HTTP/1.1,协议版本错误 +### ✅ P2-3: ProcessResponseHeaders 硬编码 HTTP/1.1,协议版本错误(已修复) -**位置**: `internal/proxy/proxy.go#L137` +**位置**: `internal/proxy/proxy.go`(原 L137) **问题描述**: -```go -tx.ProcessResponseHeaders(rw.status, "HTTP/1.1") -``` +硬编码 `"HTTP/1.1"`,HTTP/2 请求时协议版本不正确,Coraza 规则中依赖协议版本的检测可能不准确。 -硬编码 `"HTTP/1.1"`。如果原始请求是 HTTP/2(WAF TLS 默认启用 h2),上游 gateway 虽然是 HTTP/1.1,但 WAF 到客户端是 HTTP/2,这里的协议版本应该从请求中获取(`r.Proto`),否则 Coraza 规则中依赖协议版本的检测可能不准确。 - -**修复建议**: 使用 `r.Proto` 替代硬编码的 `"HTTP/1.1"`。 +**修复方案**(2026-08-01,与 WebSocket 支持一并修复): +将 `tx.ProcessResponseHeaders(rw.status, "HTTP/1.1")` 改为 `tx.ProcessResponseHeaders(rw.status, r.Proto)`,从请求中获取正确的协议版本。 --- -### P2-4: responseRecorder 不支持 http.Hijacker,WebSocket 升级无法工作 +### ✅ P2-4: responseRecorder 不支持 http.Hijacker,WebSocket 升级无法工作(已修复) -**位置**: `internal/proxy/proxy.go#L157-L180` +**位置**: `internal/proxy/proxy.go`(原 L157-L180) **问题描述**: -`responseRecorder` 嵌入了 `http.ResponseWriter` 但只实现了 `WriteHeader` 和 `Flush`,没有实现 `http.Hijacker` 接口。如果未来需要通过 WAF 代理 WebSocket 连接(如 aibox 的 WebSocket 接口需要对外暴露),Hijacker 是必须的——WebSocket 升级需要接管底层 TCP 连接。 +`responseRecorder` 嵌入了 `http.ResponseWriter` 但只实现了 `WriteHeader` 和 `Flush`,没有实现 `http.Hijacker` 接口,WebSocket 升级(需要接管底层 TCP 连接)无法工作。 -**修复建议**: 实现 Hijacker 接口,将调用委托给底层 ResponseWriter(如果它支持)。 +**修复方案**(2026-08-01): +1. 添加 `bufio` 和 `net` import +2. 给 `responseRecorder` 添加 `hijacked bool` 和 `statusSent bool` 字段 +3. 实现 `Hijack()` 方法,将调用委托给底层 `ResponseWriter.(http.Hijacker)`,成功后标记 `hijacked=true` 并设置 status=101 +4. 添加 `Write()` 方法确保首次 Write 前 WriteHeader 被调用(符合 http.ResponseWriter 约定) +5. 在 `ServeHTTP` 中检测 `rw.hijacked`,WebSocket 升级后跳过响应阶段 WAF 检测,记录 Info 日志 +6. ReverseProxy(Go 1.22)内置 WebSocket 代理支持,Hijack 后自动进行双向字节流转发 --- diff --git a/waf/internal/proxy/proxy.go b/waf/internal/proxy/proxy.go index a282404..eb49bf6 100644 --- a/waf/internal/proxy/proxy.go +++ b/waf/internal/proxy/proxy.go @@ -2,9 +2,11 @@ package proxy import ( + "bufio" "bytes" "fmt" "io" + "net" "net/http" "net/http/httputil" "net/url" @@ -170,13 +172,26 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { rw := &responseRecorder{ResponseWriter: w, status: 200} p.rp.ServeHTTP(rw, r) + // WebSocket 升级成功后连接已被 Hijack,转为二进制帧协议,无需执行 HTTP 响应阶段检测 + if rw.hijacked { + logclient.Info("waf", fmt.Sprintf("websocket_upgraded %s %s cost_ms=%d", + r.Method, r.URL.Path, time.Since(start).Milliseconds()), + map[string]any{ + "ip": r.RemoteAddr, + "method": r.Method, + "path": r.URL.Path, + "cost_ms": time.Since(start).Milliseconds(), + }) + return + } + // 响应阶段:把 upstream 的 header 喂给 WAF for k, vs := range rw.headers { for _, v := range vs { tx.AddResponseHeader(k, v) } } - tx.ProcessResponseHeaders(rw.status, "HTTP/1.1") + tx.ProcessResponseHeaders(rw.status, r.Proto) } // block 命中规则时统一返回 403,并将关键输入推送到日志库。 @@ -197,26 +212,58 @@ func (p *Proxy) block(w http.ResponseWriter, r *http.Request, ruleID int, start } // responseRecorder 记录状态码与响应头,供响应阶段 WAF 检测使用。 +// 同时实现 http.Hijacker 以支持 WebSocket 升级(ReverseProxy 在 WebSocket 握手时需要劫持底层连接)。 type responseRecorder struct { http.ResponseWriter - status int - headers http.Header + status int + statusSent bool + headers http.Header + hijacked bool } func (r *responseRecorder) WriteHeader(code int) { + if r.hijacked { + return + } r.status = code + r.statusSent = true if r.headers == nil { r.headers = http.Header{} } - // 拷贝上游响应头 + // 拷贝上游响应头(在 WriteHeader 调用时响应头已最终确定) for k, vs := range r.Header() { r.headers[k] = vs } r.ResponseWriter.WriteHeader(code) } +func (r *responseRecorder) Write(b []byte) (int, error) { + if !r.statusSent { + r.WriteHeader(http.StatusOK) + } + return r.ResponseWriter.Write(b) +} + +// Flush 实现 http.Flusher,支持 SSE/流式响应。 func (r *responseRecorder) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { f.Flush() } } + +// Hijack 实现 http.Hijacker,支持 WebSocket 升级。 +// 委托给底层 ResponseWriter 的 Hijack 方法,并标记连接已被劫持。 +func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker, ok := r.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, fmt.Errorf("underlying ResponseWriter does not support Hijacker") + } + conn, rw, err := hijacker.Hijack() + if err == nil { + r.hijacked = true + // WebSocket 升级响应是 101 Switching Protocols + r.status = http.StatusSwitchingProtocols + r.statusSent = true + } + return conn, rw, err +}