mirror of
https://github.com/pgsty/minio.git
synced 2026-10-31 21:02:06 +03:00
fix: complete bucket CORS protocol validation
Integrate the strict B3 XML, validation, checksum, wildcard, MaxAge, and Origin-null response contract with the C-prime site-replication register from #75. Preserve fail-closed metadata behavior and rejected-preflight cache variation while keeping legacy-invalid development metadata readable and repairable through a valid CORS PUT or DELETE. Add combined parser, handler, browser-response, namespace, replication, restart, and legacy-repair regressions, and update the internal design contract. Refs #75 Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -40,6 +40,7 @@ func TestPerBucketCorsPreflight(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil)
|
||||
req.Header.Set("Origin", "http://example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "X-Amz-Date")
|
||||
|
||||
handled := applyBucketCors(rec, req, cfg)
|
||||
if !handled {
|
||||
@@ -48,12 +49,25 @@ func TestPerBucketCorsPreflight(t *testing.T) {
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://example.com" {
|
||||
t.Fatalf("allow-origin = %q", got)
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("preflight status = %d", rec.Code)
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Fatalf("allow-credentials = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, PUT" {
|
||||
t.Fatalf("allow-methods = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "X-Amz-Date" {
|
||||
t.Fatalf("allow-headers = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "ETag" {
|
||||
t.Fatalf("expose-headers = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Max-Age"); got != "3000" {
|
||||
t.Fatalf("max-age = %q", got)
|
||||
}
|
||||
requireCorsVary(t, rec.Header())
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("preflight status = %d", rec.Code)
|
||||
}
|
||||
requireCorsOriginVary(t, rec.Header())
|
||||
}
|
||||
|
||||
@@ -92,20 +106,24 @@ func TestPerBucketCorsPreflightNoMatch(t *testing.T) {
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 for disallowed origin, got %d", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("rejected preflight returned allow-origin %q", got)
|
||||
}
|
||||
requireCorsVary(t, rec.Header())
|
||||
}
|
||||
|
||||
func TestPerBucketCorsPreflightWildcardOrigin(t *testing.T) {
|
||||
cfg := &cors.Config{CORSRules: []cors.Rule{{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
ExposeHeaders: []string{"ETag"},
|
||||
}}}
|
||||
func TestPerBucketCorsPreflightWildcardOriginAndZeroMaxAge(t *testing.T) {
|
||||
doc := `<CORSConfiguration><CORSRule><AllowedOrigin>*</AllowedOrigin><AllowedMethod>GET</AllowedMethod><AllowedMethod>HEAD</AllowedMethod><AllowedHeader>*</AllowedHeader><ExposeHeader>ETag</ExposeHeader><MaxAgeSeconds>0</MaxAgeSeconds></CORSRule></CORSConfiguration>`
|
||||
cfg, err := cors.ParseBucketCorsConfig(strings.NewReader(doc))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil)
|
||||
req.Header.Set("Origin", "https://app.example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", http.MethodGet)
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "RANGE")
|
||||
|
||||
if handled := applyBucketCors(rec, req, cfg); !handled {
|
||||
t.Fatal("expected preflight to be handled")
|
||||
@@ -116,12 +134,58 @@ func TestPerBucketCorsPreflightWildcardOrigin(t *testing.T) {
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Fatalf("allow-credentials = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, HEAD" {
|
||||
t.Fatalf("allow-methods = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "RANGE" {
|
||||
t.Fatalf("allow-headers = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "ETag" {
|
||||
t.Fatalf("expose-headers = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Max-Age"); got != "0" {
|
||||
t.Fatalf("max-age = %q", got)
|
||||
}
|
||||
requireCorsVary(t, rec.Header())
|
||||
}
|
||||
|
||||
func TestPerBucketCorsPreflightUsesFirstFullyMatchingRule(t *testing.T) {
|
||||
cfg := &cors.Config{CORSRules: []cors.Rule{
|
||||
{
|
||||
AllowedOrigins: []string{"https://app.example.com"},
|
||||
AllowedMethods: []string{"GET"},
|
||||
AllowedHeaders: []string{"x-a"},
|
||||
ExposeHeaders: []string{"x-rule-a"},
|
||||
MaxAgeSeconds: 1,
|
||||
},
|
||||
{
|
||||
AllowedOrigins: []string{"https://app.example.com"},
|
||||
AllowedMethods: []string{"GET", "HEAD"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
ExposeHeaders: []string{"x-rule-b"},
|
||||
MaxAgeSeconds: 2,
|
||||
},
|
||||
}}
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil)
|
||||
req.Header.Set("Origin", "https://app.example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "X-B")
|
||||
|
||||
if handled := applyBucketCors(rec, req, cfg); !handled {
|
||||
t.Fatal("expected preflight to be handled")
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "x-rule-b" {
|
||||
t.Fatalf("selected rule expose-headers = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, HEAD" {
|
||||
t.Fatalf("selected rule allow-methods = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Max-Age"); got != "2" {
|
||||
t.Fatalf("selected rule max-age = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerBucketCorsActualRequest(t *testing.T) {
|
||||
cfg := &cors.Config{CORSRules: []cors.Rule{{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@@ -237,6 +301,66 @@ func testBucketCorsNoConfigUsesGlobalFallback(_ ObjectLayer, _ string, bucket st
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerBucketCorsActualPatternOriginSupportsCredentials(t *testing.T) {
|
||||
cfg := &cors.Config{CORSRules: []cors.Rule{{
|
||||
AllowedOrigins: []string{"https://*.example.com"},
|
||||
AllowedMethods: []string{"GET"},
|
||||
}}}
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil)
|
||||
req.Header.Set("Origin", "https://app.example.com")
|
||||
|
||||
if handled := applyBucketCors(rec, req, cfg); handled {
|
||||
t.Fatal("actual request must continue")
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
|
||||
t.Fatalf("allow-origin = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Fatalf("allow-credentials = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerBucketCorsActualNullOriginSurvivesForwardingMiddleware(t *testing.T) {
|
||||
next := setBucketForwardingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
t.Run("per-bucket null origin", func(t *testing.T) {
|
||||
cfg := &cors.Config{CORSRules: []cors.Rule{{
|
||||
AllowedOrigins: []string{"null"},
|
||||
AllowedMethods: []string{"GET"},
|
||||
}}}
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil)
|
||||
req.Header.Set("Origin", "null")
|
||||
|
||||
if handled := applyBucketCors(rec, req, cfg); handled {
|
||||
t.Fatal("actual request must continue")
|
||||
}
|
||||
next.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "null" {
|
||||
t.Fatalf("allow-origin = %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Fatalf("allow-credentials = %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("legacy unmarked null origin", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.Header().Set("Access-Control-Allow-Origin", "null")
|
||||
req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil)
|
||||
|
||||
next.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Fatalf("allow-origin = %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func requireCorsVary(t *testing.T, header http.Header) {
|
||||
t.Helper()
|
||||
values := strings.Join(header.Values("Vary"), ",")
|
||||
|
||||
Reference in New Issue
Block a user