From bdd351bd157ec0321cfed8ae62b4c120ea28c44d Mon Sep 17 00:00:00 2001 From: mrchatam Date: Sat, 12 Sep 2026 13:22:15 +0330 Subject: [PATCH] fix(api): return 401 for invalid Bearer token instead of 404 (#6459) When Authorization: Bearer is present but does not match (or is disabled), respond with 401 Unauthorized so script authors can distinguish auth failure from a wrong webBasePath. Requests with no Authorization header still get 404 masking; wrong base paths continue to 404 via NoRoute. Fixes #6255 Co-authored-by: mrchatam --- internal/web/controller/api.go | 6 +++++- internal/web/controller/api_auth_test.go | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/internal/web/controller/api.go b/internal/web/controller/api.go index 483ed1d5f..c02eb2cd1 100644 --- a/internal/web/controller/api.go +++ b/internal/web/controller/api.go @@ -61,7 +61,11 @@ func (a *APIController) checkAPIAuth(c *gin.Context) { } } if !session.IsLogin(c) { - if c.GetHeader("X-Requested-With") == "XMLHttpRequest" { + // A presented Bearer token is not an anonymous scan: return 401 so + // callers can distinguish a bad/disabled token from a wrong base path + // (NoRoute still 404s). XHR keeps 401; bare unauthenticated stays 404. + authHdr := c.GetHeader("Authorization") + if strings.HasPrefix(authHdr, "Bearer ") || c.GetHeader("X-Requested-With") == "XMLHttpRequest" { c.AbortWithStatus(http.StatusUnauthorized) } else { c.AbortWithStatus(http.StatusNotFound) diff --git a/internal/web/controller/api_auth_test.go b/internal/web/controller/api_auth_test.go index 744c35f40..5bd4c64fc 100644 --- a/internal/web/controller/api_auth_test.go +++ b/internal/web/controller/api_auth_test.go @@ -219,18 +219,22 @@ func TestCheckAPIAuth_EmptyVerifiedChainsFallsThrough(t *testing.T) { } } -// TestCheckAPIAuth_RejectsUnauthenticated characterizes the reject paths: no -// bearer token and no session yields 401 for XHR callers and 404 otherwise. +// TestCheckAPIAuth_RejectsUnauthenticated characterizes the reject paths: +// no credential → 404 (masking); XHR or a presented (but invalid) Bearer → 401 +// so script authors can tell auth failure from a wrong base path. func TestCheckAPIAuth_RejectsUnauthenticated(t *testing.T) { engine, _ := newAPIAuthTestEngine(t) cases := []struct { - name string - xhr bool - want int + name string + xhr bool + bearer string // empty = omit Authorization header + want int }{ - {"xhr gets 401", true, http.StatusUnauthorized}, - {"non-xhr gets 404", false, http.StatusNotFound}, + {"xhr gets 401", true, "", http.StatusUnauthorized}, + {"non-xhr gets 404", false, "", http.StatusNotFound}, + {"invalid bearer gets 401", false, "definitely-not-a-token", http.StatusUnauthorized}, + {"invalid bearer xhr gets 401", true, "definitely-not-a-token", http.StatusUnauthorized}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -238,6 +242,9 @@ func TestCheckAPIAuth_RejectsUnauthenticated(t *testing.T) { if c.xhr { req.Header.Set("X-Requested-With", "XMLHttpRequest") } + if c.bearer != "" { + req.Header.Set("Authorization", "Bearer "+c.bearer) + } w := httptest.NewRecorder() engine.ServeHTTP(w, req) if w.Code != c.want {