From bfc59ad1206d4cbfcce83c0171a8d208c3973546 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani <45800463+rchincha@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:14:52 -0700 Subject: [PATCH] security: suppress Allow-Credentials on wildcard CORS origin (CORS-1) (#3980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(security): suppress Allow-Credentials on wildcard CORS origin (CORS-1) Per CORS spec §3.2, Access-Control-Allow-Credentials must not be "true" when Access-Control-Allow-Origin is the wildcard "*". ACHeadersMiddleware (pkg/common/http_server.go) and getUIHeadersHandler (pkg/api/routes.go) now only emit the credentials header when an explicit, non-empty AllowOrigin is configured. Deployments that leave AllowOrigin blank (default wildcard) no longer produce a contradictory header pair. Signed-off-by: Ramkumar Chinchani --- pkg/api/routes.go | 10 +++++++--- pkg/api/routes_test.go | 32 ++++++++++++++++++++++++++++++++ pkg/common/http_server.go | 7 +++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index c86e840c..1d3bffe5 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -248,9 +248,12 @@ func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(h response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName) - // Get auth config safely + // Access-Control-Allow-Credentials must not be "true" when + // Access-Control-Allow-Origin is the wildcard "*" (CORS spec §3.2). + // Only advertise credentials support when an explicit origin is set. authConfig := config.CopyAuthConfig() - if authConfig.IsBasicAuthnEnabled() { + allowOrigin := strings.TrimSpace(config.GetAllowOrigin()) + if authConfig.IsBasicAuthnEnabled() && allowOrigin != "" && allowOrigin != "*" { response.Header().Set("Access-Control-Allow-Credentials", "true") } @@ -517,7 +520,8 @@ type ExtensionList struct { func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.Request) { // Get auth config safely authConfig := rh.c.Config.CopyAuthConfig() - if authConfig.IsBasicAuthnEnabled() { + allowOrigin := strings.TrimSpace(rh.c.Config.GetAllowOrigin()) + if authConfig.IsBasicAuthnEnabled() && allowOrigin != "" && allowOrigin != "*" { response.Header().Set("Access-Control-Allow-Credentials", "true") } diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index 2d706cd7..05da2575 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -241,6 +241,38 @@ func TestRoutes(t *testing.T) { defer resp.Body.Close() So(resp, ShouldNotBeNil) + So(resp.Header.Get("Access-Control-Allow-Credentials"), ShouldEqual, "") + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + }) + + Convey("Get manifest with explicit AllowOrigin emits credentials header", func() { + ctlr.StoreController.DefaultStore = &mocks.MockedImageStore{ + GetImageManifestFn: func(repo string, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "", "", zerr.ErrRepoBadVersion + }, + } + + originalAllowOrigin := ctlr.Config.HTTP.AllowOrigin + ctlr.Config.HTTP.AllowOrigin = "https://example.com" + + defer func() { + ctlr.Config.HTTP.AllowOrigin = originalAllowOrigin + }() + + request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil) + request = mux.SetURLVars(request, map[string]string{ + "name": "test", + "reference": "b8b1231908844a55c251211c7a67ae3c809fb86a081a8eeb4a715e6d7d65625c", + }) + response := httptest.NewRecorder() + + rthdlr.GetManifest(response, request) + + resp := response.Result() + + defer resp.Body.Close() + So(resp, ShouldNotBeNil) + So(resp.Header.Get("Access-Control-Allow-Credentials"), ShouldEqual, "true") So(resp.StatusCode, ShouldEqual, http.StatusNotFound) }) diff --git a/pkg/common/http_server.go b/pkg/common/http_server.go index 0b1d6e30..f2965077 100644 --- a/pkg/common/http_server.go +++ b/pkg/common/http_server.go @@ -38,9 +38,12 @@ func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.Mi resp.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue) resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName) - // Get auth config safely + // Access-Control-Allow-Credentials must not be "true" when + // Access-Control-Allow-Origin is the wildcard "*" (CORS spec §3.2). + // Only advertise credentials support when an explicit origin is set. authConfig := config.CopyAuthConfig() - if authConfig.IsBasicAuthnEnabled() { + allowOrigin := strings.TrimSpace(config.GetAllowOrigin()) + if authConfig.IsBasicAuthnEnabled() && allowOrigin != "" && allowOrigin != "*" { resp.Header().Set("Access-Control-Allow-Credentials", "true") }