From cf8b1a9e53f8829273534d3e05884aa513c18214 Mon Sep 17 00:00:00 2001 From: samwaf Date: Wed, 16 Sep 2026 14:34:22 +0800 Subject: [PATCH] fix: treat a literal null token as no token at all #938 --- middleware/auth_api_check.go | 24 ++++++++++++++++----- middleware/auth_ws_token_test.go | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/middleware/auth_api_check.go b/middleware/auth_api_check.go index b15e6e80..d126efc2 100644 --- a/middleware/auth_api_check.go +++ b/middleware/auth_api_check.go @@ -56,7 +56,7 @@ func Auth() gin.HandlerFunc { // 获取请求头中 token,实际是一个完整被签名过的 token;a complete, signed token tokenStr := extractTokenStr(c) if tokenStr == "" { - zlog.Debug("无token") + zlog.Debug(fmt.Sprintf("请求未携带令牌 path:%v 来源IP:%v", c.Request.URL.Path, utils.GetManageClientIP(c))) response.AuthFailWithMessage("鉴权失败", c) c.Abort() @@ -238,15 +238,29 @@ func bindFailCounterTTL() time.Duration { func extractTokenStr(c *gin.Context) string { reqPath := c.Request.URL.Path if reqPath == "/api/v1/ws" { - return c.GetHeader("Sec-WebSocket-Protocol") + return normalizeTokenStr(c.GetHeader("Sec-WebSocket-Protocol")) } if strings.HasPrefix(reqPath, "/api/v1/waflog/attack/download") { - return c.Query("X-Token") + return normalizeTokenStr(c.Query("X-Token")) } if c.GetHeader("X-Login-Type") == "mobile" { - return c.GetHeader("X-Mobile-Token") + return normalizeTokenStr(c.GetHeader("X-Mobile-Token")) } - return c.GetHeader("X-Token") + return normalizeTokenStr(c.GetHeader("X-Token")) +} + +// normalizeTokenStr 把"没有令牌"的几种写法统一成空串。 +// +// 字面量 null/undefined 要当成没有:WebSocket 握手带不了自定义头,令牌只能放进子协议, +// 而浏览器会把 JS 里的 null 原样字符串化发出来。不归一的话它是个非空字符串, +// 会一路走到缓存查询并被判成"令牌不存在"——日志上看就像有人拿着无效令牌反复试, +// 实际只是登录页在重连。 +func normalizeTokenStr(raw string) string { + token := strings.TrimSpace(raw) + if token == "null" || token == "undefined" { + return "" + } + return token } // isFingerprintExemptPath 判断当前请求是否豁免设备指纹比对。 diff --git a/middleware/auth_ws_token_test.go b/middleware/auth_ws_token_test.go index 6c6f486d..a66a1732 100644 --- a/middleware/auth_ws_token_test.go +++ b/middleware/auth_ws_token_test.go @@ -112,3 +112,39 @@ func TestFingerprintExemptWithQueryString(t *testing.T) { }) } } + +// 未登录时前端从 localStorage 取不到令牌,浏览器会把 JS 的 null 原样字符串化成 "null" +// 放进子协议发出来。不归一的话它是个非空字符串,会一路走到缓存查询并被判成"令牌不存在", +// 登录页停着不动也会每隔几秒刷一条无效令牌日志。 +func TestExtractTokenStrTreatsNullLiteralAsEmpty(t *testing.T) { + cases := []struct { + name string + target string + header string + value string + }{ + {"WebSocket 子协议 null", "/api/v1/ws", "Sec-WebSocket-Protocol", "null"}, + {"WebSocket 子协议 undefined", "/api/v1/ws", "Sec-WebSocket-Protocol", "undefined"}, + {"WebSocket 子协议空白", "/api/v1/ws", "Sec-WebSocket-Protocol", " "}, + {"常规请求头 null", "/api/v1/host/list", "X-Token", "null"}, + {"常规请求头 undefined", "/api/v1/host/list", "X-Token", "undefined"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := ctxFor(t, http.MethodGet, tc.target, map[string]string{tc.header: tc.value}) + if got := extractTokenStr(c); got != "" { + t.Fatalf("%q 应被当作没有令牌,实际取到 %q", tc.value, got) + } + }) + } +} + +// 归一化不能误伤正常令牌 +func TestExtractTokenStrKeepsRealToken(t *testing.T) { + c := ctxFor(t, http.MethodGet, "/api/v1/host/list", map[string]string{ + "X-Token": "0d42ce0c1f2a3b4c5d6e7f8091a2b3c4", + }) + if got := extractTokenStr(c); got != "0d42ce0c1f2a3b4c5d6e7f8091a2b3c4" { + t.Fatalf("正常令牌被改动了:%q", got) + } +}