mirror of https://github.com/minio/minio.git
Compare commits
2 Commits
6940e0121e
...
d74ae22ac3
Author | SHA1 | Date |
---|---|---|
|
d74ae22ac3 | |
|
af3d0e0bfb |
|
@ -683,7 +683,8 @@ func corsHandler(handler http.Handler) http.Handler {
|
|||
// Configure CORS dynamically based on current settings
|
||||
// This ensures we handle configuration changes and wildcard security properly
|
||||
hasWildcard := configHasWildcard()
|
||||
|
||||
allowCredentialsWithWildcard := globalAPIConfig.getCorsAllowCredentialsWithWildcard()
|
||||
|
||||
opts := cors.Options{
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
for _, allowedOrigin := range globalAPIConfig.getCorsAllowOrigins() {
|
||||
|
@ -702,11 +703,12 @@ func corsHandler(handler http.Handler) http.Handler {
|
|||
http.MethodOptions,
|
||||
http.MethodPatch,
|
||||
},
|
||||
AllowedHeaders: commonS3Headers,
|
||||
ExposedHeaders: commonS3Headers,
|
||||
AllowedHeaders: commonS3Headers,
|
||||
ExposedHeaders: commonS3Headers,
|
||||
// CORS spec compliance: disable credentials when wildcard origins are configured
|
||||
// Unless explicitly overridden by administrator (for backward compatibility)
|
||||
// This prevents the security vulnerability where any website can make credentialed requests
|
||||
AllowCredentials: !hasWildcard,
|
||||
AllowCredentials: !hasWildcard || allowCredentialsWithWildcard,
|
||||
}
|
||||
|
||||
// Use rs/cors directly without custom wrapper to avoid interface issues
|
||||
|
|
|
@ -27,59 +27,83 @@ import (
|
|||
func TestCORSCredentialsWithWildcard(t *testing.T) {
|
||||
// Save original config and restore after test
|
||||
originalOrigins := globalAPIConfig.getCorsAllowOrigins()
|
||||
originalAllowCredentialsWithWildcard := globalAPIConfig.getCorsAllowCredentialsWithWildcard()
|
||||
defer func() {
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = originalOrigins
|
||||
globalAPIConfig.corsAllowCredentialsWithWildcard = originalAllowCredentialsWithWildcard
|
||||
globalAPIConfig.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Setup wildcard CORS config
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = []string{"*"}
|
||||
globalAPIConfig.mu.Unlock()
|
||||
// Test 1: Default secure behavior (wildcard without credentials)
|
||||
t.Run("Secure Default", func(t *testing.T) {
|
||||
// Setup wildcard CORS config with default secure behavior
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = []string{"*"}
|
||||
globalAPIConfig.corsAllowCredentialsWithWildcard = false // default secure behavior
|
||||
globalAPIConfig.mu.Unlock()
|
||||
|
||||
// Create a simple handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Create a simple handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with CORS handler
|
||||
corsWrappedHandler := corsHandler(handler)
|
||||
|
||||
// Test preflight request
|
||||
req := httptest.NewRequest("OPTIONS", "/", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
corsWrappedHandler.ServeHTTP(rr, req)
|
||||
|
||||
// Verify specific origin is echoed back (rs/cors library behavior)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin: https://example.com, got: %s", got)
|
||||
}
|
||||
|
||||
// Verify credentials header is NOT present (security fix)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Errorf("Expected no Access-Control-Allow-Credentials header with wildcard origin, got: %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
// Wrap with CORS handler
|
||||
corsWrappedHandler := corsHandler(handler)
|
||||
// Test 2: Backward compatibility opt-out (wildcard with credentials - insecure)
|
||||
t.Run("Backward Compatibility Opt-out", func(t *testing.T) {
|
||||
// Setup wildcard CORS config with explicit opt-out for backward compatibility
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = []string{"*"}
|
||||
globalAPIConfig.corsAllowCredentialsWithWildcard = true // explicitly allow insecure behavior
|
||||
globalAPIConfig.mu.Unlock()
|
||||
|
||||
// Test preflight request
|
||||
req := httptest.NewRequest("OPTIONS", "/", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
// Create a simple handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
corsWrappedHandler.ServeHTTP(rr, req)
|
||||
// Wrap with CORS handler
|
||||
corsWrappedHandler := corsHandler(handler)
|
||||
|
||||
// Verify specific origin is echoed back (rs/cors library behavior)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin: https://example.com, got: %s", got)
|
||||
}
|
||||
// Test preflight request
|
||||
req := httptest.NewRequest("OPTIONS", "/", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
|
||||
// Verify credentials header is NOT present (security fix)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Errorf("Expected no Access-Control-Allow-Credentials header with wildcard origin, got: %s", got)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
corsWrappedHandler.ServeHTTP(rr, req)
|
||||
|
||||
// Test actual GET request
|
||||
req = httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
// Verify specific origin is echoed back (rs/cors library behavior)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin: https://example.com, got: %s", got)
|
||||
}
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
corsWrappedHandler.ServeHTTP(rr, req)
|
||||
|
||||
// Verify specific origin is echoed back (rs/cors library behavior)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin: https://example.com, got: %s", got)
|
||||
}
|
||||
|
||||
// Verify credentials header is NOT present
|
||||
if got := rr.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Errorf("Expected no Access-Control-Allow-Credentials header with wildcard origin, got: %s", got)
|
||||
}
|
||||
// Verify credentials header IS present (backward compatibility)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Errorf("Expected Access-Control-Allow-Credentials: true with opt-out enabled, got: %s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test that CORS credentials are allowed for specific origins
|
||||
|
@ -168,7 +192,7 @@ func TestCORSUnauthorizedOrigin(t *testing.T) {
|
|||
|
||||
// Test preflight request from unauthorized origin
|
||||
req := httptest.NewRequest("OPTIONS", "/", nil)
|
||||
req.Header.Set("Origin", "https://example.org") // This origin is NOT in the allowed list
|
||||
req.Header.Set("Origin", "https://example.org") // This origin is NOT in the allowed list
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -188,15 +212,18 @@ func TestCORSUnauthorizedOrigin(t *testing.T) {
|
|||
func TestCORSMixedConfiguration(t *testing.T) {
|
||||
// Save original config and restore after test
|
||||
originalOrigins := globalAPIConfig.getCorsAllowOrigins()
|
||||
originalAllowCredentialsWithWildcard := globalAPIConfig.getCorsAllowCredentialsWithWildcard()
|
||||
defer func() {
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = originalOrigins
|
||||
globalAPIConfig.corsAllowCredentialsWithWildcard = originalAllowCredentialsWithWildcard
|
||||
globalAPIConfig.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Setup mixed CORS config (wildcard + specific origins)
|
||||
// Setup mixed CORS config (wildcard + specific origins) with default secure behavior
|
||||
globalAPIConfig.mu.Lock()
|
||||
globalAPIConfig.corsAllowOrigins = []string{"*", "https://example.com"}
|
||||
globalAPIConfig.corsAllowCredentialsWithWildcard = false // default secure behavior
|
||||
globalAPIConfig.mu.Unlock()
|
||||
|
||||
// Create a simple handler
|
||||
|
@ -220,7 +247,7 @@ func TestCORSMixedConfiguration(t *testing.T) {
|
|||
t.Errorf("Expected Access-Control-Allow-Origin: https://example.com, got: %s", got)
|
||||
}
|
||||
|
||||
// Verify credentials header is NOT present due to wildcard in config
|
||||
// Verify credentials header is NOT present due to wildcard in config (secure default)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Errorf("Expected no Access-Control-Allow-Credentials header with wildcard in config, got: %s", got)
|
||||
}
|
||||
|
|
|
@ -49,14 +49,15 @@ type apiConfig struct {
|
|||
replicationMaxLWorkers int
|
||||
transitionWorkers int
|
||||
|
||||
staleUploadsExpiry time.Duration
|
||||
staleUploadsCleanupInterval time.Duration
|
||||
deleteCleanupInterval time.Duration
|
||||
enableODirect bool
|
||||
gzipObjects bool
|
||||
rootAccess bool
|
||||
syncEvents bool
|
||||
objectMaxVersions int64
|
||||
staleUploadsExpiry time.Duration
|
||||
staleUploadsCleanupInterval time.Duration
|
||||
deleteCleanupInterval time.Duration
|
||||
enableODirect bool
|
||||
gzipObjects bool
|
||||
rootAccess bool
|
||||
syncEvents bool
|
||||
objectMaxVersions int64
|
||||
corsAllowCredentialsWithWildcard bool
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -187,6 +188,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int, legacy bool) {
|
|||
t.rootAccess = cfg.RootAccess
|
||||
t.syncEvents = cfg.SyncEvents
|
||||
t.objectMaxVersions = cfg.ObjectMaxVersions
|
||||
t.corsAllowCredentialsWithWildcard = cfg.CorsAllowCredentialsWithWildcard
|
||||
|
||||
if t.staleUploadsCleanupInterval != cfg.StaleUploadsCleanupInterval {
|
||||
t.staleUploadsCleanupInterval = cfg.StaleUploadsCleanupInterval
|
||||
|
@ -244,6 +246,12 @@ func (t *apiConfig) getCorsAllowOrigins() []string {
|
|||
return corsAllowOrigins
|
||||
}
|
||||
|
||||
func (t *apiConfig) getCorsAllowCredentialsWithWildcard() bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.corsAllowCredentialsWithWildcard
|
||||
}
|
||||
|
||||
func (t *apiConfig) getStaleUploadsCleanupInterval() time.Duration {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
|
|
@ -32,14 +32,15 @@ import (
|
|||
|
||||
// API sub-system constants
|
||||
const (
|
||||
apiRequestsMax = "requests_max"
|
||||
apiClusterDeadline = "cluster_deadline"
|
||||
apiCorsAllowOrigin = "cors_allow_origin"
|
||||
apiRemoteTransportDeadline = "remote_transport_deadline"
|
||||
apiListQuorum = "list_quorum"
|
||||
apiReplicationPriority = "replication_priority"
|
||||
apiReplicationMaxWorkers = "replication_max_workers"
|
||||
apiReplicationMaxLWorkers = "replication_max_lrg_workers"
|
||||
apiRequestsMax = "requests_max"
|
||||
apiClusterDeadline = "cluster_deadline"
|
||||
apiCorsAllowOrigin = "cors_allow_origin"
|
||||
apiCorsAllowCredentialsWithWildcard = "cors_allow_credentials_with_wildcard"
|
||||
apiRemoteTransportDeadline = "remote_transport_deadline"
|
||||
apiListQuorum = "list_quorum"
|
||||
apiReplicationPriority = "replication_priority"
|
||||
apiReplicationMaxWorkers = "replication_max_workers"
|
||||
apiReplicationMaxLWorkers = "replication_max_lrg_workers"
|
||||
|
||||
apiTransitionWorkers = "transition_workers"
|
||||
apiStaleUploadsCleanupInterval = "stale_uploads_cleanup_interval"
|
||||
|
@ -52,17 +53,18 @@ const (
|
|||
apiSyncEvents = "sync_events"
|
||||
apiObjectMaxVersions = "object_max_versions"
|
||||
|
||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||
EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
|
||||
EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN"
|
||||
EnvAPIRemoteTransportDeadline = "MINIO_API_REMOTE_TRANSPORT_DEADLINE"
|
||||
EnvAPITransitionWorkers = "MINIO_API_TRANSITION_WORKERS"
|
||||
EnvAPIListQuorum = "MINIO_API_LIST_QUORUM"
|
||||
EnvAPISecureCiphers = "MINIO_API_SECURE_CIPHERS" // default config.EnableOn
|
||||
EnvAPIReplicationPriority = "MINIO_API_REPLICATION_PRIORITY"
|
||||
EnvAPIReplicationMaxWorkers = "MINIO_API_REPLICATION_MAX_WORKERS"
|
||||
EnvAPIReplicationMaxLWorkers = "MINIO_API_REPLICATION_MAX_LRG_WORKERS"
|
||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||
EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
|
||||
EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN"
|
||||
EnvAPICorsAllowCredentialsWithWildcard = "MINIO_API_CORS_ALLOW_CREDENTIALS_WITH_WILDCARD"
|
||||
EnvAPIRemoteTransportDeadline = "MINIO_API_REMOTE_TRANSPORT_DEADLINE"
|
||||
EnvAPITransitionWorkers = "MINIO_API_TRANSITION_WORKERS"
|
||||
EnvAPIListQuorum = "MINIO_API_LIST_QUORUM"
|
||||
EnvAPISecureCiphers = "MINIO_API_SECURE_CIPHERS" // default config.EnableOn
|
||||
EnvAPIReplicationPriority = "MINIO_API_REPLICATION_PRIORITY"
|
||||
EnvAPIReplicationMaxWorkers = "MINIO_API_REPLICATION_MAX_WORKERS"
|
||||
EnvAPIReplicationMaxLWorkers = "MINIO_API_REPLICATION_MAX_LRG_WORKERS"
|
||||
|
||||
EnvAPIStaleUploadsCleanupInterval = "MINIO_API_STALE_UPLOADS_CLEANUP_INTERVAL"
|
||||
EnvAPIStaleUploadsExpiry = "MINIO_API_STALE_UPLOADS_EXPIRY"
|
||||
|
@ -100,6 +102,10 @@ var (
|
|||
Key: apiCorsAllowOrigin,
|
||||
Value: "*",
|
||||
},
|
||||
config.KV{
|
||||
Key: apiCorsAllowCredentialsWithWildcard,
|
||||
Value: config.EnableOn,
|
||||
},
|
||||
config.KV{
|
||||
Key: apiRemoteTransportDeadline,
|
||||
Value: "2h",
|
||||
|
@ -166,23 +172,24 @@ var (
|
|||
|
||||
// Config storage class configuration
|
||||
type Config struct {
|
||||
RequestsMax int `json:"requests_max"`
|
||||
ClusterDeadline time.Duration `json:"cluster_deadline"`
|
||||
CorsAllowOrigin []string `json:"cors_allow_origin"`
|
||||
RemoteTransportDeadline time.Duration `json:"remote_transport_deadline"`
|
||||
ListQuorum string `json:"list_quorum"`
|
||||
ReplicationPriority string `json:"replication_priority"`
|
||||
ReplicationMaxWorkers int `json:"replication_max_workers"`
|
||||
ReplicationMaxLWorkers int `json:"replication_max_lrg_workers"`
|
||||
TransitionWorkers int `json:"transition_workers"`
|
||||
StaleUploadsCleanupInterval time.Duration `json:"stale_uploads_cleanup_interval"`
|
||||
StaleUploadsExpiry time.Duration `json:"stale_uploads_expiry"`
|
||||
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
|
||||
EnableODirect bool `json:"enable_odirect"`
|
||||
GzipObjects bool `json:"gzip_objects"`
|
||||
RootAccess bool `json:"root_access"`
|
||||
SyncEvents bool `json:"sync_events"`
|
||||
ObjectMaxVersions int64 `json:"object_max_versions"`
|
||||
RequestsMax int `json:"requests_max"`
|
||||
ClusterDeadline time.Duration `json:"cluster_deadline"`
|
||||
CorsAllowOrigin []string `json:"cors_allow_origin"`
|
||||
CorsAllowCredentialsWithWildcard bool `json:"cors_allow_credentials_with_wildcard"`
|
||||
RemoteTransportDeadline time.Duration `json:"remote_transport_deadline"`
|
||||
ListQuorum string `json:"list_quorum"`
|
||||
ReplicationPriority string `json:"replication_priority"`
|
||||
ReplicationMaxWorkers int `json:"replication_max_workers"`
|
||||
ReplicationMaxLWorkers int `json:"replication_max_lrg_workers"`
|
||||
TransitionWorkers int `json:"transition_workers"`
|
||||
StaleUploadsCleanupInterval time.Duration `json:"stale_uploads_cleanup_interval"`
|
||||
StaleUploadsExpiry time.Duration `json:"stale_uploads_expiry"`
|
||||
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
|
||||
EnableODirect bool `json:"enable_odirect"`
|
||||
GzipObjects bool `json:"gzip_objects"`
|
||||
RootAccess bool `json:"root_access"`
|
||||
SyncEvents bool `json:"sync_events"`
|
||||
ObjectMaxVersions int64 `json:"object_max_versions"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
|
||||
|
@ -211,11 +218,13 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
|||
enableODirect := env.Get(EnvAPIODirect, kvs.Get(apiODirect)) == config.EnableOn
|
||||
gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn
|
||||
rootAccess := env.Get(EnvAPIRootAccess, kvs.Get(apiRootAccess)) == config.EnableOn
|
||||
corsAllowCredentialsWithWildcard := env.Get(EnvAPICorsAllowCredentialsWithWildcard, kvs.Get(apiCorsAllowCredentialsWithWildcard)) == config.EnableOn
|
||||
|
||||
cfg = Config{
|
||||
EnableODirect: enableODirect || !disableODirect,
|
||||
GzipObjects: gzipObjects,
|
||||
RootAccess: rootAccess,
|
||||
EnableODirect: enableODirect || !disableODirect,
|
||||
GzipObjects: gzipObjects,
|
||||
RootAccess: rootAccess,
|
||||
CorsAllowCredentialsWithWildcard: corsAllowCredentialsWithWildcard,
|
||||
}
|
||||
|
||||
var corsAllowOrigin []string
|
||||
|
|
|
@ -44,6 +44,12 @@ var (
|
|||
Optional: true,
|
||||
Type: "csv",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: apiCorsAllowCredentialsWithWildcard,
|
||||
Description: `allow credentials with wildcard CORS origins (default: 'on' for backward compatibility, 'off' for enhanced security compliance)` + defaultHelpPostfix(apiCorsAllowCredentialsWithWildcard),
|
||||
Optional: true,
|
||||
Type: "on|off",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: apiRemoteTransportDeadline,
|
||||
Description: `set the deadline for API requests on remote transports while proxying between federated instances e.g. "2h"` + defaultHelpPostfix(apiRemoteTransportDeadline),
|
||||
|
|
Loading…
Reference in New Issue