From ac0806e33e8346b6ac6728f09ab7704604d9818e Mon Sep 17 00:00:00 2001 From: cczj Date: Mon, 26 Oct 2026 17:45:23 +0800 Subject: [PATCH] fix(auth): match Basic authentication schemes case-insensitively --- auth.go | 8 ++++-- auth_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index 5d3222d566..863928da89 100644 --- a/auth.go +++ b/auth.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin/internal/bytesconv" ) @@ -30,11 +31,14 @@ type authPair struct { type authPairs []authPair func (a authPairs) searchCredential(authValue string) (string, bool) { - if authValue == "" { + const prefix = "Basic " + // Authentication schemes are case-insensitive (RFC 9110, Section 11.1). + if len(authValue) < len(prefix) || !strings.EqualFold(authValue[:len(prefix)], prefix) { return "", false } + authValue = authValue[len(prefix):] for _, pair := range a { - if subtle.ConstantTimeCompare(bytesconv.StringToBytes(pair.value), bytesconv.StringToBytes(authValue)) == 1 { + if subtle.ConstantTimeCompare(bytesconv.StringToBytes(pair.value[len(prefix):]), bytesconv.StringToBytes(authValue)) == 1 { return pair.user, true } } diff --git a/auth_test.go b/auth_test.go index 9166e3b0f1..8d92fbca4b 100644 --- a/auth_test.go +++ b/auth_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -81,6 +82,80 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) { assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) } +func TestBasicAuthSchemeCase(t *testing.T) { + accounts := Accounts{"admin": "password"} + credentials := strings.TrimPrefix(authorizationHeader("admin", "password"), "Basic ") + tests := []struct { + name string + header string + valid bool + }{ + {name: "canonical", header: "Basic " + credentials, valid: true}, + {name: "lowercase", header: "basic " + credentials, valid: true}, + {name: "uppercase", header: "BASIC " + credentials, valid: true}, + {name: "mixed case", header: "bAsIc " + credentials, valid: true}, + {name: "empty"}, + {name: "missing credentials", header: "Basic "}, + {name: "missing space", header: "Basic" + credentials}, + {name: "tab separator", header: "Basic\t" + credentials}, + {name: "other scheme", header: "Bearer " + credentials}, + {name: "longer scheme", header: "BasicX " + credentials}, + {name: "non-ASCII scheme", header: "ba\u017fic " + credentials}, + {name: "credential case", header: "basic " + strings.ToLower(credentials)}, + {name: "username case", header: authorizationHeader("Admin", "password")}, + {name: "password case", header: authorizationHeader("admin", "Password")}, + {name: "malformed credentials", header: "basic !invalid!"}, + } + middlewares := []struct { + name string + handler HandlerFunc + header string + userKey string + challenge string + realm string + status int + }{ + { + name: "origin", handler: BasicAuth(accounts), + header: "Authorization", userKey: AuthUserKey, + challenge: "WWW-Authenticate", realm: `Basic realm="Authorization Required"`, + status: http.StatusUnauthorized, + }, + { + name: "proxy", handler: BasicAuthForProxy(accounts, ""), + header: "Proxy-Authorization", userKey: AuthProxyUserKey, + challenge: "Proxy-Authenticate", realm: `Basic realm="Proxy Authorization Required"`, + status: http.StatusProxyAuthRequired, + }, + } + for _, middleware := range middlewares { + t.Run(middleware.name, func(t *testing.T) { + router := New() + router.Use(middleware.handler) + router.GET("/", func(c *Context) { + c.String(http.StatusOK, c.MustGet(middleware.userKey).(string)) + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(middleware.header, tt.header) + router.ServeHTTP(w, req) + if tt.valid { + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "admin", w.Body.String()) + assert.Empty(t, w.Header().Get(middleware.challenge)) + } else { + assert.Equal(t, middleware.status, w.Code) + assert.Empty(t, w.Body.String()) + assert.Equal(t, middleware.realm, w.Header().Get(middleware.challenge)) + } + }) + } + }) + } +} + func TestBasicAuthSucceed(t *testing.T) { accounts := Accounts{"admin": "password"} router := New()