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 5f28c1580c1b..8f5a1e7f421b 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
@@ -116,6 +116,14 @@
*/
String allowCredentials() default "";
+ /**
+ * Whether private network access is supported. Please, see
+ * {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details.
+ *
By default this is not set (i.e. private network access is not supported).
+ * @since 6.1.3
+ */
+ String allowPrivateNetwork() default "";
+
/**
* The maximum age (in seconds) of the cache duration for preflight responses.
*
This property controls the value of the {@code Access-Control-Max-Age}
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 88ffe1bc96bd..4f09598cca0b 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
@@ -90,6 +90,9 @@ public class CorsConfiguration {
@Nullable
private Boolean allowCredentials;
+ @Nullable
+ private Boolean allowPrivateNetwork;
+
@Nullable
private Long maxAge;
@@ -114,6 +117,7 @@ public CorsConfiguration(CorsConfiguration other) {
this.allowedHeaders = other.allowedHeaders;
this.exposedHeaders = other.exposedHeaders;
this.allowCredentials = other.allowCredentials;
+ this.allowPrivateNetwork = other.allowPrivateNetwork;
this.maxAge = other.maxAge;
}
@@ -133,9 +137,10 @@ public CorsConfiguration(CorsConfiguration other) {
* {@code Access-Control-Allow-Origin} response header is set either to the
* matched domain value or to {@code "*"}. Keep in mind however that the
* CORS spec does not allow {@code "*"} when {@link #setAllowCredentials
- * allowCredentials} is set to {@code true} and as of 5.3 that combination
- * is rejected in favor of using {@link #setAllowedOriginPatterns
- * allowedOriginPatterns} instead.
+ * allowCredentials} is set to {@code true}, and does not recommend {@code "*"}
+ * when {@link #setAllowPrivateNetwork allowPrivateNetwork} is set to {@code true}.
+ * As a consequence, those combinations are rejected in favor of using
+ * {@link #setAllowedOriginPatterns allowedOriginPatterns} instead.
*
By default this is not set which means that no origins are allowed.
* However, an instance of this class is often initialized further, e.g. for
* {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}.
@@ -199,11 +204,13 @@ else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(th
* note that such placeholders must be resolved externally.
*
*
In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which
- * only supports "*" and cannot be used with {@code allowCredentials}, when
- * an allowedOriginPattern is matched, the {@code Access-Control-Allow-Origin}
- * response header is set to the matched origin and not to {@code "*"} nor
- * to the pattern. Therefore, allowedOriginPatterns can be used in combination
- * with {@link #setAllowCredentials} set to {@code true}.
+ * only supports "*" and cannot be used with {@code allowCredentials} or
+ * {@code allowPrivateNetwork}, when an {@code allowedOriginPattern} is matched,
+ * the {@code Access-Control-Allow-Origin} response header is set to the
+ * matched origin and not to {@code "*"} nor to the pattern.
+ * Therefore, {@code allowedOriginPatterns} can be used in combination with
+ * {@link #setAllowCredentials} and {@link #setAllowPrivateNetwork} set to
+ * {@code true}
*
By default this is not set.
* @since 5.3
*/
@@ -461,6 +468,33 @@ public Boolean getAllowCredentials() {
return this.allowCredentials;
}
+ /**
+ * Whether private network access is supported for user-agents restricting such access by default.
+ *
Private network requests are requests whose target server's IP address is more private than
+ * that from which the request initiator was fetched. For example, a request from a public website
+ * (https://example.com) to a private website (https://router.local), or a request from a private
+ * website to localhost.
+ *
Setting this property has an impact on how {@link #setAllowedOrigins(List)
+ * origins} and {@link #setAllowedOriginPatterns(List) originPatterns} are processed,
+ * see related API documentation for more details.
+ *
By default this is not set (i.e. private network access is not supported).
+ * @since 6.1.3
+ * @see Private network access specifications
+ */
+ public void setAllowPrivateNetwork(@Nullable Boolean allowPrivateNetwork) {
+ this.allowPrivateNetwork = allowPrivateNetwork;
+ }
+
+ /**
+ * Return the configured {@code allowPrivateNetwork} flag, or {@code null} if none.
+ * @since 6.1.3
+ * @see #setAllowPrivateNetwork(Boolean)
+ */
+ @Nullable
+ public Boolean getAllowPrivateNetwork() {
+ return this.allowPrivateNetwork;
+ }
+
/**
* Configure how long, as a duration, the response from a pre-flight request
* can be cached by clients.
@@ -543,6 +577,25 @@ public void validateAllowCredentials() {
}
}
+ /**
+ * Validate that when {@link #setAllowPrivateNetwork allowPrivateNetwork} is {@code true},
+ * {@link #setAllowedOrigins allowedOrigins} does not contain the special
+ * value {@code "*"} since this is insecure.
+ * @throws IllegalArgumentException if the validation fails
+ * @since 6.1.3
+ */
+ public void validateAllowPrivateNetwork() {
+ if (this.allowPrivateNetwork == Boolean.TRUE &&
+ this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) {
+
+ throw new IllegalArgumentException(
+ "When allowPrivateNetwork is true, allowedOrigins cannot contain the special value \"*\" " +
+ "as it is not recommended from a security perspective. " +
+ "To allow private network access to a set of origins, list them explicitly " +
+ "or consider using \"allowedOriginPatterns\" instead.");
+ }
+ }
+
/**
* Combine the non-null properties of the supplied
* {@code CorsConfiguration} with this one.
@@ -577,6 +630,10 @@ public CorsConfiguration combine(@Nullable CorsConfiguration other) {
if (allowCredentials != null) {
config.setAllowCredentials(allowCredentials);
}
+ Boolean allowPrivateNetwork = other.getAllowPrivateNetwork();
+ if (allowPrivateNetwork != null) {
+ config.setAllowPrivateNetwork(allowPrivateNetwork);
+ }
Long maxAge = other.getMaxAge();
if (maxAge != null) {
config.setMaxAge(maxAge);
@@ -640,6 +697,7 @@ public String checkOrigin(@Nullable String origin) {
if (!ObjectUtils.isEmpty(this.allowedOrigins)) {
if (this.allowedOrigins.contains(ALL)) {
validateAllowCredentials();
+ validateAllowPrivateNetwork();
return ALL;
}
for (String allowedOrigin : this.allowedOrigins) {
diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java
index 177d28b5a63d..c134d806df9b 100644
--- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java
+++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java
@@ -54,6 +54,18 @@ public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
+ /**
+ * The {@code Access-Control-Request-Private-Network} request header field name.
+ * @see Private Network Access specification
+ */
+ static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network";
+
+ /**
+ * The {@code Access-Control-Allow-Private-Network} response header field name.
+ * @see Private Network Access specification
+ */
+ static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network";
+
@Override
@SuppressWarnings("resource")
@@ -155,6 +167,11 @@ protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse r
responseHeaders.setAccessControlAllowCredentials(true);
}
+ if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) &&
+ Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) {
+ responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true));
+ }
+
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java
index 8f6b16f1f7b4..a259efbb8e74 100644
--- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java
+++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java
@@ -52,6 +52,18 @@ public class DefaultCorsProcessor implements CorsProcessor {
private static final List VARY_HEADERS = List.of(
HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
+ /**
+ * The {@code Access-Control-Request-Private-Network} request header field name.
+ * @see Private Network Access specification
+ */
+ static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network";
+
+ /**
+ * The {@code Access-Control-Allow-Private-Network} response header field name.
+ * @see Private Network Access specification
+ */
+ static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network";
+
@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
@@ -153,6 +165,11 @@ protected boolean handleInternal(ServerWebExchange exchange,
responseHeaders.setAccessControlAllowCredentials(true);
}
+ if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) &&
+ Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) {
+ responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true));
+ }
+
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
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 567240558273..01c925488fa8 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-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -50,6 +50,8 @@ void setNullValues() {
assertThat(config.getExposedHeaders()).isNull();
config.setAllowCredentials(null);
assertThat(config.getAllowCredentials()).isNull();
+ config.setAllowPrivateNetwork(null);
+ assertThat(config.getAllowPrivateNetwork()).isNull();
config.setMaxAge((Long) null);
assertThat(config.getMaxAge()).isNull();
}
@@ -63,6 +65,7 @@ void setValues() {
config.addAllowedMethod("*");
config.addExposedHeader("*");
config.setAllowCredentials(true);
+ config.setAllowPrivateNetwork(true);
config.setMaxAge(123L);
assertThat(config.getAllowedOrigins()).containsExactly("*");
@@ -71,6 +74,7 @@ void setValues() {
assertThat(config.getAllowedMethods()).containsExactly("*");
assertThat(config.getExposedHeaders()).containsExactly("*");
assertThat(config.getAllowCredentials()).isTrue();
+ assertThat(config.getAllowPrivateNetwork()).isTrue();
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
}
@@ -93,6 +97,7 @@ void combineWithNullProperties() {
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
+ config.setAllowPrivateNetwork(true);
CorsConfiguration other = new CorsConfiguration();
config = config.combine(other);
@@ -105,6 +110,7 @@ void combineWithNullProperties() {
assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name());
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
assertThat(config.getAllowCredentials()).isTrue();
+ assertThat(config.getAllowPrivateNetwork()).isTrue();
}
@Test // SPR-15772
@@ -258,6 +264,7 @@ void combine() {
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
+ config.setAllowPrivateNetwork(true);
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain2.com");
@@ -267,6 +274,7 @@ void combine() {
other.addAllowedMethod(HttpMethod.PUT.name());
other.setMaxAge(456L);
other.setAllowCredentials(false);
+ other.setAllowPrivateNetwork(false);
config = config.combine(other);
assertThat(config).isNotNull();
@@ -277,6 +285,7 @@ void combine() {
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(456));
assertThat(config).isNotNull();
assertThat(config.getAllowCredentials()).isFalse();
+ assertThat(config.getAllowPrivateNetwork()).isFalse();
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com");
}
diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
index 735504aa204a..2bd442867cfb 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
@@ -351,6 +351,32 @@ public void preflightRequestCredentialsWithWildcardOrigin() throws Exception {
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
+ @Test
+ public void preflightRequestPrivateNetworkWithWildcardOrigin() throws Exception {
+ this.request.setMethod(HttpMethod.OPTIONS.name());
+ this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
+ this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1");
+ this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
+ this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example"));
+ this.conf.addAllowedHeader("Header1");
+ this.conf.setAllowPrivateNetwork(true);
+
+ assertThatIllegalArgumentException().isThrownBy(() ->
+ this.processor.processRequest(this.conf, this.request, this.response));
+
+ this.conf.setAllowedOrigins(null);
+ this.conf.addAllowedOriginPattern("*");
+
+ this.processor.processRequest(this.conf, this.request, this.response);
+ assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
+ assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
+ assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN,
+ HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
+ assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+ }
+
@Test
public void preflightRequestAllowedHeaders() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
@@ -434,4 +460,49 @@ public void preventDuplicatedVaryHeaders() throws Exception {
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
+ @Test
+ public void preflightRequestWithoutAccessControlRequestPrivateNetwork() throws Exception {
+ this.request.setMethod(HttpMethod.OPTIONS.name());
+ this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
+ this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+
+ this.processor.processRequest(this.conf, this.request, this.response);
+ assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
+ assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() throws Exception {
+ this.request.setMethod(HttpMethod.OPTIONS.name());
+ this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
+ this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+
+ this.processor.processRequest(this.conf, this.request, this.response);
+ assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
+ assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() throws Exception {
+ this.request.setMethod(HttpMethod.OPTIONS.name());
+ this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
+ this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+ this.conf.setAllowPrivateNetwork(true);
+
+ this.processor.processRequest(this.conf, this.request, this.response);
+ assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
+ assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
index eb7a97ecab0f..87ee906f86e6 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java
@@ -364,6 +364,33 @@ public void preflightRequestCredentialsWithWildcardOrigin() {
assertThat((Object) response.getStatusCode()).isNull();
}
+ @Test
+ public void preflightRequestPrivateNetworkWithWildcardOrigin() {
+ ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
+ .header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
+ .header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1")
+ .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
+
+ this.conf.addAllowedOrigin("https://domain1.com");
+ this.conf.addAllowedOrigin("*");
+ this.conf.addAllowedOrigin("http://domain3.example");
+ this.conf.addAllowedHeader("Header1");
+ this.conf.setAllowPrivateNetwork(true);
+ assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange));
+
+ this.conf.setAllowedOrigins(null);
+ this.conf.addAllowedOriginPattern("*");
+ this.processor.process(this.conf, exchange);
+
+ ServerHttpResponse response = exchange.getResponse();
+ assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
+ assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
+ assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
+ ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
+ assertThat(response.getStatusCode()).isNull();
+ }
+
@Test
public void preflightRequestAllowedHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
@@ -460,6 +487,57 @@ public void preventDuplicatedVaryHeaders() {
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
}
+ @Test
+ public void preflightRequestWithoutAccessControlRequestPrivateNetwork() {
+ ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
+ .header(ACCESS_CONTROL_REQUEST_METHOD, "GET"));
+
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+
+ this.processor.process(this.conf, exchange);
+
+ ServerHttpResponse response = exchange.getResponse();
+ assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
+ assertThat(response.getStatusCode()).isNull();
+ }
+
+ @Test
+ public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() {
+ ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
+ .header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
+ .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
+
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+
+ this.processor.process(this.conf, exchange);
+
+ ServerHttpResponse response = exchange.getResponse();
+ assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
+ assertThat(response.getStatusCode()).isNull();
+ }
+
+ @Test
+ public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() {
+ ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
+ .header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
+ .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
+
+ this.conf.addAllowedHeader("*");
+ this.conf.addAllowedOrigin("https://domain2.com");
+ this.conf.setAllowPrivateNetwork(true);
+
+ this.processor.process(this.conf, exchange);
+
+ ServerHttpResponse response = exchange.getResponse();
+ assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
+ assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
+ assertThat(response.getStatusCode()).isNull();
+ }
+
private ServerWebExchange actualRequest() {
return MockServerWebExchange.from(corsRequest(HttpMethod.GET));
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 383505c4c7fa..32c15d439914 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
@@ -131,6 +131,17 @@ public CorsRegistration allowCredentials(boolean allowCredentials) {
return this;
}
+ /**
+ * Whether private network access is supported.
+ *
Please, see {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details.
+ *
By default this is not set (i.e. private network access is not supported).
+ * @since 6.1.3
+ */
+ public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) {
+ this.config.setAllowPrivateNetwork(allowPrivateNetwork);
+ return this;
+ }
+
/**
* Configure how long in seconds the response from a pre-flight request
* can be cached by clients.
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java
index 391695382b4f..de09dba809c9 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java
@@ -196,6 +196,7 @@ public Mono