From 8c3a05bbcf210aa38a375b1668a9cba56db2d7e4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 19 Nov 2020 09:20:55 +0000 Subject: [PATCH] Allow "*" for Access-Control-Expose-Headers Closes gh-26113 --- .../web/bind/annotation/CrossOrigin.java | 2 ++ .../web/cors/CorsConfiguration.java | 12 +++------ .../web/cors/CorsConfigurationTests.java | 27 +++++++++---------- .../web/reactive/config/CorsRegistration.java | 3 ++- .../config/annotation/CorsRegistration.java | 3 ++- .../web/servlet/config/spring-mvc.xsd | 1 + 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java index ca755049783..bb49357aab4 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java @@ -114,6 +114,8 @@ public @interface CrossOrigin { * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, *

Exposed headers are listed in the {@code Access-Control-Expose-Headers} * response header of actual CORS requests. + *

The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

By default no headers are listed as exposed. */ String[] exposedHeaders() default {}; 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 6ca27ac9bab..68252e68933 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 @@ -329,13 +329,11 @@ public class CorsConfiguration { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an * actual response might have and can be exposed. - *

Note that {@code "*"} is not a valid exposed header value. + *

The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

By default this is not set. */ public void setExposedHeaders(@Nullable List exposedHeaders) { - if (exposedHeaders != null && exposedHeaders.contains(ALL)) { - throw new IllegalArgumentException("'*' is not a valid exposed header value"); - } this.exposedHeaders = (exposedHeaders != null ? new ArrayList<>(exposedHeaders) : null); } @@ -351,12 +349,10 @@ public class CorsConfiguration { /** * Add a response header to expose. - *

Note that {@code "*"} is not a valid exposed header value. + *

The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. */ public void addExposedHeader(String exposedHeader) { - if (ALL.equals(exposedHeader)) { - throw new IllegalArgumentException("'*' is not a valid exposed header value"); - } if (this.exposedHeaders == null) { this.exposedHeaders = new ArrayList<>(4); } 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 446fd81c013..82c5286dce7 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 @@ -61,8 +61,7 @@ public class CorsConfigurationTests { config.addAllowedOriginPattern("http://*.example.com"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); - config.addExposedHeader("header1"); - config.addExposedHeader("header2"); + config.addExposedHeader("*"); config.setAllowCredentials(true); config.setMaxAge(123L); @@ -70,23 +69,11 @@ public class CorsConfigurationTests { assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.example.com"); assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(config.getAllowedMethods()).containsExactly("*"); - assertThat(config.getExposedHeaders()).containsExactly("header1", "header2"); + assertThat(config.getExposedHeaders()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(new Long(123)); } - @Test - public void asteriskWildCardOnAddExposedHeader() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CorsConfiguration().addExposedHeader("*")); - } - - @Test - public void asteriskWildCardOnSetExposedHeaders() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CorsConfiguration().setExposedHeaders(Collections.singletonList("*"))); - } - @Test public void combineWithNull() { CorsConfiguration config = new CorsConfiguration(); @@ -133,12 +120,14 @@ public class CorsConfigurationTests { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = other.combine(config); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = config.combine(new CorsConfiguration()); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -146,6 +135,7 @@ public class CorsConfigurationTests { assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = new CorsConfiguration().combine(config); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -153,6 +143,7 @@ public class CorsConfigurationTests { assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); } @Test @@ -196,6 +187,7 @@ public class CorsConfigurationTests { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); + config.addExposedHeader("*"); config.addAllowedMethod("*"); config.addAllowedOriginPattern("*"); @@ -204,6 +196,8 @@ public class CorsConfigurationTests { other.addAllowedOriginPattern("http://*.company.com"); other.addAllowedHeader("header1"); other.addExposedHeader("header2"); + other.addAllowedHeader("anotherHeader1"); + other.addExposedHeader("anotherHeader2"); other.addAllowedMethod(HttpMethod.PUT.name()); CorsConfiguration combinedConfig = config.combine(other); @@ -211,6 +205,7 @@ public class CorsConfigurationTests { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*"); assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); + assertThat(combinedConfig.getExposedHeaders()).containsExactly("*"); assertThat(combinedConfig.getAllowedMethods()).containsExactly("*"); combinedConfig = other.combine(config); @@ -218,7 +213,9 @@ public class CorsConfigurationTests { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*"); assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); + assertThat(combinedConfig.getExposedHeaders()).containsExactly("*"); assertThat(combinedConfig.getAllowedMethods()).containsExactly("*"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); } @Test // SPR-14792 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index d459eaef1e7..ce7aa013032 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -98,7 +98,8 @@ public class CorsRegistration { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an * actual response might have and can be exposed. - *

Note that {@code "*"} is not supported on this property. + *

The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index 71542289803..0de5ecae5db 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -99,7 +99,8 @@ public class CorsRegistration { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an * actual response might have and can be exposed. - *

Note that {@code "*"} is not supported on this property. + *

The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd index b604caae745..c0364d28c12 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd @@ -1400,6 +1400,7 @@ Comma-separated list of response headers other than simple headers (i.e. Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) that an actual response might have and can be exposed. + The special value "*" allows all headers to be exposed for non-credentialed requests. Empty by default. ]]>