From 3ed315fea36725e0da654ceeee3ad107d0f6d469 Mon Sep 17 00:00:00 2001 From: Wei-Lun Li Date: Sat, 31 Oct 2026 04:37:01 +0000 Subject: [PATCH] Retain CORS default permit values in combine() Prior to this commit, CorsConfiguration#combine() kept the default permit value for allowed origins set by applyPermitDefaultValues() but passed the default permit values for allowed methods and allowed headers through setAllowedMethods() and setAllowedHeaders(), which copy them. The combined configuration then no longer recognized them as default values, so combining it again with explicitly defined values merged them instead of overriding them: allowed headers stayed "*" and allowed methods kept GET, HEAD and POST next to the explicit ones. This happens, for example, when CorsRegistration#combine() is used for a global CORS mapping that is later combined with the configuration of a `@CrossOrigin` handler method. This commit retains the default permit values for allowed methods and allowed headers in combine(), as already done for allowed origins, so that explicitly defined values still override them. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Wei-Lun Li --- .../web/cors/CorsConfiguration.java | 19 ++++++++-- .../web/cors/CorsConfigurationTests.java | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index f3864c6a2df6..aa7cdf710e64 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -603,8 +603,23 @@ public CorsConfiguration combine(@Nullable CorsConfiguration other) { List patterns = combinePatterns(this.allowedOriginPatterns, other.allowedOriginPatterns); config.allowedOrigins = (origins == DEFAULT_PERMIT_ALL && !CollectionUtils.isEmpty(patterns) ? null : origins); config.allowedOriginPatterns = patterns; - config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods())); - config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders())); + // Bypass setAllowedMethods and setAllowedHeaders to retain default permit values + List methods = combine(getAllowedMethods(), other.getAllowedMethods()); + if (methods == DEFAULT_PERMIT_METHODS) { + config.allowedMethods = methods; + config.resolvedMethods = (this.allowedMethods == methods ? + this.resolvedMethods : other.resolvedMethods); + } + else { + config.setAllowedMethods(methods); + } + List headers = combine(getAllowedHeaders(), other.getAllowedHeaders()); + if (headers == DEFAULT_PERMIT_ALL) { + config.allowedHeaders = headers; + } + else { + config.setAllowedHeaders(headers); + } config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders())); Boolean allowCredentials = other.getAllowCredentials(); if (allowCredentials != null) { diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 6bc920185474..1111d49bb58f 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -152,6 +152,41 @@ void combineWithDefaultPermitValues() { assertThat(combinedConfig.getExposedHeaders()).isEmpty(); } + @Test + void combineWithDefaultPermitValuesOfCombinedConfig() { + CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues(); + CorsConfiguration other = new CorsConfiguration(); + other.addAllowedOrigin("https://domain.com"); + other.addAllowedHeader("header1"); + other.addAllowedMethod(HttpMethod.PUT.name()); + + CorsConfiguration combinedConfig = config.combine(new CorsConfiguration()).combine(other); + assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); + assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.checkHttpMethod(HttpMethod.POST)).isNull(); + + combinedConfig = other.combine(config.combine(new CorsConfiguration())); + assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); + assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.checkHttpMethod(HttpMethod.POST)).isNull(); + + combinedConfig = new CorsConfiguration().combine(config).combine(other); + assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); + assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.checkHttpMethod(HttpMethod.POST)).isNull(); + + combinedConfig = new CorsConfiguration().combine(config); + assertThat(combinedConfig.checkHttpMethod(HttpMethod.POST)) + .containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST); + combinedConfig.addAllowedHeader("header1"); + combinedConfig.addAllowedMethod("PATCH"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*", "header1"); + assertThat(combinedConfig.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH"); + } + @Test void combinePatternWithDefaultPermitValues() { CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();