From 3e4ba757162cbd6eb9e9f2714ee6a04d1fe4c38c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 19 Nov 2020 09:48:29 +0000 Subject: [PATCH] Allow "*" for Access-Control-Expose-Headers Closes gh-26113 --- .../web/bind/annotation/CrossOrigin.java | 4 ++- .../web/cors/CorsConfiguration.java | 14 ++++------ .../web/cors/CorsConfigurationTests.java | 28 ++++++++----------- .../web/reactive/config/CorsRegistration.java | 5 ++-- .../config/annotation/CorsRegistration.java | 5 ++-- .../web/servlet/config/spring-mvc.xsd | 1 + 6 files changed, 27 insertions(+), 30 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 7fb1b88c4d1..a7c7e7474c9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,6 +112,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 0b86523081d..82ec0c02ae1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -253,13 +253,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); } @@ -275,12 +273,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 e188f8d80b2..685890cfb4e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,27 +60,14 @@ public class CorsConfigurationTests { assertEquals(Arrays.asList("*"), config.getAllowedHeaders()); config.addAllowedMethod("*"); assertEquals(Arrays.asList("*"), config.getAllowedMethods()); - config.addExposedHeader("header1"); - config.addExposedHeader("header2"); - assertEquals(Arrays.asList("header1", "header2"), config.getExposedHeaders()); + config.addExposedHeader("*"); + assertEquals(Arrays.asList("*"), config.getAllowedMethods()); config.setAllowCredentials(true); assertTrue(config.getAllowCredentials()); config.setMaxAge(123L); assertEquals(new Long(123), config.getMaxAge()); } - @Test(expected = IllegalArgumentException.class) - public void asteriskWildCardOnAddExposedHeader() { - CorsConfiguration config = new CorsConfiguration(); - config.addExposedHeader("*"); - } - - @Test(expected = IllegalArgumentException.class) - public void asteriskWildCardOnSetExposedHeaders() { - CorsConfiguration config = new CorsConfiguration(); - config.setExposedHeaders(Arrays.asList("*")); - } - @Test public void combineWithNull() { CorsConfiguration config = new CorsConfiguration(); @@ -120,23 +107,27 @@ public class CorsConfigurationTests { assertEquals(Arrays.asList("https://domain.com"), combinedConfig.getAllowedOrigins()); assertEquals(Arrays.asList("header1"), combinedConfig.getAllowedHeaders()); assertEquals(Arrays.asList(HttpMethod.PUT.name()), combinedConfig.getAllowedMethods()); + assertEquals(Collections.emptyList(), combinedConfig.getExposedHeaders()); combinedConfig = other.combine(config); assertEquals(Arrays.asList("https://domain.com"), combinedConfig.getAllowedOrigins()); assertEquals(Arrays.asList("header1"), combinedConfig.getAllowedHeaders()); assertEquals(Arrays.asList(HttpMethod.PUT.name()), combinedConfig.getAllowedMethods()); + assertEquals(Collections.emptyList(), combinedConfig.getExposedHeaders()); combinedConfig = config.combine(new CorsConfiguration()); assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); assertEquals(Arrays.asList("*"), config.getAllowedHeaders()); assertEquals(Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()), combinedConfig.getAllowedMethods()); + assertEquals(Collections.emptyList(), combinedConfig.getExposedHeaders()); combinedConfig = new CorsConfiguration().combine(config); assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); assertEquals(Arrays.asList("*"), config.getAllowedHeaders()); assertEquals(Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()), combinedConfig.getAllowedMethods()); + assertEquals(Collections.emptyList(), combinedConfig.getExposedHeaders()); } @Test @@ -144,19 +135,24 @@ public class CorsConfigurationTests { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); + config.addExposedHeader("*"); config.addAllowedMethod("*"); CorsConfiguration other = new CorsConfiguration(); other.addAllowedOrigin("https://domain.com"); other.addAllowedHeader("header1"); other.addExposedHeader("header2"); + other.addAllowedHeader("anotherHeader1"); + other.addExposedHeader("anotherHeader2"); other.addAllowedMethod(HttpMethod.PUT.name()); CorsConfiguration combinedConfig = config.combine(other); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedOrigins()); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedHeaders()); + assertEquals(Arrays.asList("*"), combinedConfig.getExposedHeaders()); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedMethods()); combinedConfig = other.combine(config); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedOrigins()); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedHeaders()); + assertEquals(Arrays.asList("*"), combinedConfig.getExposedHeaders()); assertEquals(Arrays.asList("*"), combinedConfig.getAllowedMethods()); } 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 096935e79da..06301f82f25 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,7 +93,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 a7bb9371abc..b30748fbfd4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,7 +95,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 7c6fd031df8..cd3d37e7e17 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 @@ -1363,6 +1363,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. ]]>