From ec2979ca48b190e122e62d65bdf724047330d11b Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 2 Sep 2026 18:49:17 +0800 Subject: [PATCH] fix: keep the CORS lookup fail-closed until bucket metadata is loaded Restore the startup guard removed by the previous cleanup: while bucket metadata is still loading, a non-resident name may be a bucket with a restrictive CORS document, so the request gets no CORS answer instead of the global policy. After startup a non-resident name still falls back to the global policy without any metadata I/O; the separate load-failure set stays removed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PvgysXDmhPBBimCReYtA8q Signed-off-by: Feng Ruohang --- cmd/api-router.go | 7 ++++--- cmd/bucket-cors-middleware_test.go | 10 +++++----- cmd/bucket-metadata-sys.go | 13 +++++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cmd/api-router.go b/cmd/api-router.go index 0f98159db..af223f2d5 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -789,9 +789,10 @@ func corsHandler(handler http.Handler) http.Handler { if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil { // Resident-only lookup: this runs before authentication with a // client-supplied path segment as the bucket name, so it must - // never load or cache metadata. A bucket with a stored CORS - // document that failed to parse gets no CORS headers; any other - // non-resident name falls back to the global policy below. + // never load or cache metadata. While startup loading is still + // running, and for a bucket whose stored CORS document failed to + // parse, the request gets no CORS headers; any other non-resident + // name falls back to the global policy below. cfg, _, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket) if err == nil && cfg != nil { if applyBucketCors(w, r, cfg) { diff --git a/cmd/bucket-cors-middleware_test.go b/cmd/bucket-cors-middleware_test.go index 33ffc7dc6..9c389435a 100644 --- a/cmd/bucket-cors-middleware_test.go +++ b/cmd/bucket-cors-middleware_test.go @@ -598,15 +598,15 @@ func testBucketCorsUnknownBucketDoesNotGrowMetadata(obj ObjectLayer, _ string, _ } } -func TestBucketCorsStartupMissUsesGlobalFallbackWithoutIO(t *testing.T) { +func TestBucketCorsStartupMissFailsClosedWithoutIO(t *testing.T) { ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ t: t, - objAPITest: testBucketCorsStartupMissUsesGlobalFallbackWithoutIO, + objAPITest: testBucketCorsStartupMissFailsClosedWithoutIO, endpoints: []string{"GetBucketCors"}, }) } -func testBucketCorsStartupMissUsesGlobalFallbackWithoutIO(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { +func testBucketCorsStartupMissFailsClosedWithoutIO(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { oldObjectAPI := newObjectLayerFn() oldMetadataSys := globalBucketMetadataSys counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} @@ -630,8 +630,8 @@ func testBucketCorsStartupMissUsesGlobalFallbackWithoutIO(obj ObjectLayer, _ str if !innerCalled || rec.Code != http.StatusNoContent { t.Fatalf("startup miss did not reach inner handler: called=%v status=%d", innerCalled, rec.Code) } - if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { - t.Fatalf("startup miss did not fall back to the global policy: %q", got) + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("startup miss used permissive global CORS: %q", got) } if got := counting.getObjectNInfoCalls.Load(); got != 0 { t.Fatalf("startup miss performed %d metadata reads", got) diff --git a/cmd/bucket-metadata-sys.go b/cmd/bucket-metadata-sys.go index 982b86e42..973ddeb67 100644 --- a/cmd/bucket-metadata-sys.go +++ b/cmd/bucket-metadata-sys.go @@ -400,18 +400,23 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC // GetResidentCorsConfig returns the CORS configuration of a bucket whose // metadata is already resident in memory. It runs before authentication for // every Origin-bearing request with a client-supplied path segment, so it -// never loads or caches metadata. A name that is not resident, including any -// real bucket while startup loading is still in progress, reports -// errConfigNotFound and the caller applies the global CORS policy exactly as -// releases without per-bucket CORS did. +// never loads or caches metadata. Until startup loading has completed, a +// non-resident name may still be a bucket with a restrictive document, so it +// reports errBucketMetadataNotInitialized and gets no CORS answer. After +// that, a non-resident name reports errConfigNotFound and the caller applies +// the global CORS policy exactly as releases without per-bucket CORS did. func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config, time.Time, error) { if isReservedOrInvalidBucket(bucket, true) { return nil, time.Time{}, errConfigNotFound } sys.RLock() meta, ok := sys.metadataMap[bucket] + initialized := sys.initialized sys.RUnlock() if !ok { + if !initialized { + return nil, time.Time{}, errBucketMetadataNotInitialized + } return nil, time.Time{}, errConfigNotFound } if meta.corsConfigErr != nil {