Browse Source

fix(s3api): missing `Vary: Origin` header on non-CORS and `OPTIONS` requests (#8072)

* fix: Refactor CORS middleware to consistently apply the `Vary: Origin` header when a configuration exists and streamline request processing logic.

* fix: Add Vary: Origin header to CORS OPTIONS responses and refactor request handling for clarity and correctness.

* fix: update CORS middleware tests to correctly parse and check for 'Origin' in Vary header.

* refactor: extract `hasVaryOrigin` helper function to simplify Vary header checks in tests.

* test: Remove `Vary: Origin` header from CORS test expectations.

* refactor: consolidate CORS request handling into a new `processCORS` method using a `next` callback.
pull/7934/merge
粒粒橙 2 days ago
committed by GitHub
parent
commit
52882aed70
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      weed/s3api/cors/cors.go
  2. 2
      weed/s3api/cors/cors_test.go
  3. 117
      weed/s3api/cors/middleware.go
  4. 159
      weed/s3api/cors/middleware_test.go

4
weed/s3api/cors/cors.go

@ -361,10 +361,6 @@ func ApplyHeaders(w http.ResponseWriter, corsResp *CORSResponse) {
if corsResp.AllowOrigin != "" { if corsResp.AllowOrigin != "" {
w.Header().Set("Access-Control-Allow-Origin", corsResp.AllowOrigin) w.Header().Set("Access-Control-Allow-Origin", corsResp.AllowOrigin)
if corsResp.AllowOrigin != "*" {
w.Header().Add("Vary", "Origin")
}
} }
if corsResp.AllowMethods != "" { if corsResp.AllowMethods != "" {

2
weed/s3api/cors/cors_test.go

@ -523,7 +523,6 @@ func TestApplyHeaders(t *testing.T) {
"Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Expose-Headers": "ETag", "Access-Control-Expose-Headers": "ETag",
"Access-Control-Max-Age": "3600", "Access-Control-Max-Age": "3600",
"Vary": "Origin",
}, },
}, },
{ {
@ -537,7 +536,6 @@ func TestApplyHeaders(t *testing.T) {
"Access-Control-Allow-Origin": "http://example.com", "Access-Control-Allow-Origin": "http://example.com",
"Access-Control-Allow-Methods": "GET", "Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Credentials": "true",
"Vary": "Origin",
}, },
}, },
} }

117
weed/s3api/cors/middleware.go

@ -69,103 +69,56 @@ func (m *Middleware) getCORSConfig(bucket string) (*CORSConfiguration, bool) {
// Handler returns the CORS middleware handler // Handler returns the CORS middleware handler
func (m *Middleware) Handler(next http.Handler) http.Handler { func (m *Middleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Parse CORS request
corsReq := ParseRequest(r)
// If not a CORS request, continue normally
if corsReq.Origin == "" {
next.ServeHTTP(w, r)
return
}
// Extract bucket from request
bucket, _ := s3_constants.GetBucketAndObject(r)
if bucket == "" {
next.ServeHTTP(w, r)
return
}
// Get CORS configuration (bucket-specific or fallback) BEFORE checking bucket existence
// This ensures CORS headers are applied consistently regardless of bucket existence,
// preventing information disclosure about whether a bucket exists
config, found := m.getCORSConfig(bucket)
if !found {
// No CORS configuration at all, handle based on request type
if corsReq.IsPreflightRequest {
// Preflight request without CORS config should fail
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
return
}
// Non-preflight request, continue normally
next.ServeHTTP(w, r)
return
}
// Evaluate CORS request
corsResp, err := EvaluateRequest(config, corsReq)
if err != nil {
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
if corsReq.IsPreflightRequest {
// Preflight request that doesn't match CORS rules should fail
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
return
}
// Non-preflight request, continue normally but without CORS headers
m.processCORS(w, r, func() {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return
}
// Apply CORS headers early, before bucket existence check
// This ensures consistent CORS behavior and prevents information disclosure
ApplyHeaders(w, corsResp)
// Handle preflight requests - return success immediately without checking bucket existence
// This matches AWS S3 behavior where preflight requests succeed even for non-existent buckets
if corsReq.IsPreflightRequest {
// Preflight request should return 200 OK with just CORS headers
w.WriteHeader(http.StatusOK)
return
}
// For actual requests, continue with normal processing
// The handler will check bucket existence and return appropriate errors (e.g., NoSuchBucket)
// but CORS headers have already been applied above
next.ServeHTTP(w, r)
})
}) })
} }
// HandleOptionsRequest handles OPTIONS requests for CORS preflight // HandleOptionsRequest handles OPTIONS requests for CORS preflight
func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) { func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) {
// Parse CORS request
m.processCORS(w, r, func() {
w.WriteHeader(http.StatusOK)
})
}
// processCORS handles the common CORS logic for both regular and preflight requests.
// It takes a next callback which is executed if the request flow proceeds (i.e., not short-circuited by CORS errors or preflight responses).
func (m *Middleware) processCORS(w http.ResponseWriter, r *http.Request, next func()) {
corsReq := ParseRequest(r) corsReq := ParseRequest(r)
bucket, _ := s3_constants.GetBucketAndObject(r)
// If not a CORS request, return OK
if corsReq.Origin == "" {
w.WriteHeader(http.StatusOK)
// 1. Basic Validation
if bucket == "" {
next()
return return
} }
// Extract bucket from request
bucket, _ := s3_constants.GetBucketAndObject(r)
if bucket == "" {
w.WriteHeader(http.StatusOK)
// 2. Load Configuration
config, hasConfig := m.getCORSConfig(bucket)
// 3. Apply Vary Header (Always applied if config exists)
if hasConfig {
w.Header().Add("Vary", "Origin")
}
// 4. Handle Non-CORS Requests
if corsReq.Origin == "" {
next()
return return
} }
// Get CORS configuration (bucket-specific or fallback) BEFORE checking bucket existence
// This ensures CORS headers are applied consistently regardless of bucket existence
config, found := m.getCORSConfig(bucket)
if !found {
// No CORS configuration at all for OPTIONS request should return access denied
// 5. Handle Missing Configuration
if !hasConfig {
if corsReq.IsPreflightRequest { if corsReq.IsPreflightRequest {
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
return return
} }
w.WriteHeader(http.StatusOK)
next()
return return
} }
// Evaluate CORS request
// 6. Evaluate CORS Request
corsResp, err := EvaluateRequest(config, corsReq) corsResp, err := EvaluateRequest(config, corsReq)
if err != nil { if err != nil {
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err) glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
@ -173,11 +126,17 @@ func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
return return
} }
w.WriteHeader(http.StatusOK)
next()
return return
} }
// Apply CORS headers and return success
// 7. Success Case
ApplyHeaders(w, corsResp) ApplyHeaders(w, corsResp)
w.WriteHeader(http.StatusOK)
if corsReq.IsPreflightRequest {
w.WriteHeader(http.StatusOK)
return
}
next()
} }

159
weed/s3api/cors/middleware_test.go

@ -3,6 +3,7 @@ package cors
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -403,3 +404,161 @@ func TestMiddlewareFallbackWithError(t *testing.T) {
}) })
} }
} }
// TestMiddlewareVaryHeader tests that the Vary: Origin header is correctly applied in various scenarios
func TestMiddlewareVaryHeader(t *testing.T) {
tests := []struct {
name string
bucketConfig *CORSConfiguration
shouldHaveVaryHeader bool
description string
}{
{
name: "Specific allowed origin",
bucketConfig: &CORSConfiguration{
CORSRules: []CORSRule{
{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
AllowedHeaders: []string{"*"},
},
},
},
shouldHaveVaryHeader: true,
description: "Should have Vary: Origin header when CORS config exists with specific origin",
},
{
name: "Wildcard allowed origin",
bucketConfig: &CORSConfiguration{
CORSRules: []CORSRule{
{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET"},
AllowedHeaders: []string{"*"},
},
},
},
shouldHaveVaryHeader: true,
description: "Should have Vary: Origin header even when CORS config has wildcard origin",
},
{
name: "No CORS configuration",
bucketConfig: nil,
shouldHaveVaryHeader: false,
description: "Should NOT have Vary: Origin header when no CORS config exists",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mocks
bucketChecker := &mockBucketChecker{bucketExists: true}
var errCode s3err.ErrorCode
if tt.bucketConfig == nil {
errCode = s3err.ErrNoSuchCORSConfiguration
} else {
errCode = s3err.ErrNone
}
configGetter := &mockCORSConfigGetter{
config: tt.bucketConfig,
errCode: errCode,
}
// Create middleware
middleware := NewMiddleware(bucketChecker, configGetter, nil)
// Create request WITHOUT Origin header
req := httptest.NewRequest("GET", "/testbucket/testobject", nil)
req = mux.SetURLVars(req, map[string]string{
"bucket": "testbucket",
"object": "testobject",
})
// Create response recorder
w := httptest.NewRecorder()
// Create a simple handler that returns 200 OK
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Execute middleware
middleware.Handler(nextHandler).ServeHTTP(w, req)
// Check Vary header
hasVaryOrigin := hasVaryOrigin(w.Header())
if tt.shouldHaveVaryHeader && !hasVaryOrigin {
t.Errorf("%s: expected Vary: Origin header, but got %v", tt.description, w.Header()["Vary"])
} else if !tt.shouldHaveVaryHeader && hasVaryOrigin {
t.Errorf("%s: expected NO Vary: Origin header, but got it", tt.description)
}
})
}
}
// TestHandleOptionsRequestVaryHeader reproduces the issue where HandleOptionsRequest misses Vary: Origin
func TestHandleOptionsRequestVaryHeader(t *testing.T) {
// Setup mocks
bucketChecker := &mockBucketChecker{bucketExists: true}
config := &CORSConfiguration{
CORSRules: []CORSRule{
{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"*"},
},
},
}
configGetter := &mockCORSConfigGetter{
config: config,
errCode: s3err.ErrNone,
}
// Create middleware
middleware := NewMiddleware(bucketChecker, configGetter, nil)
// Create OPTIONS request
req := httptest.NewRequest("OPTIONS", "/testbucket/testobject", nil)
req = mux.SetURLVars(req, map[string]string{
"bucket": "testbucket",
"object": "testobject",
})
// Set valid CORS headers
req.Header.Set("Origin", "https://example.com")
req.Header.Set("Access-Control-Request-Method", "GET")
// Create response recorder
w := httptest.NewRecorder()
// Execute HandleOptionsRequest
middleware.HandleOptionsRequest(w, req)
// Check Response Status
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Check Vary header - verified to FAIL without fix
if !hasVaryOrigin(w.Header()) {
t.Errorf("Expected Vary: Origin header in OPTIONS response, but got %v", w.Header()["Vary"])
}
}
// hasVaryOrigin checks if the "Vary" header contains "Origin"
func hasVaryOrigin(header http.Header) bool {
varyHeaders := header["Vary"]
for _, h := range varyHeaders {
for _, v := range strings.Split(h, ",") {
if strings.TrimSpace(v) == "Origin" {
return true
}
}
}
return false
}
Loading…
Cancel
Save