diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc index 6bc7077f2c..f90bd5a19c 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc @@ -5,6 +5,9 @@ The `SecureHeaders` `GatewayFilter` factory adds a number of headers to the resp The following headers (shown with their default values) are added: +NOTE: If a `Content-Security-Policy` header is already present (for example, from an upstream service), the value configured by `SecureHeaders` is appended as an additional `Content-Security-Policy` header value. +Other secure headers are only added when absent. + * `X-Xss-Protection:1 (mode=block`) * `Strict-Transport-Security (max-age=631138519`) * `X-Frame-Options (DENY)` diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java index 40ba973c67..6eb45f95ea 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java @@ -143,8 +143,8 @@ private void applySecurityHeaders(HttpHeaders responseHeaders, Set heade addHeaderIfEnabled(responseHeaders, headersToAddToResponse, SecureHeadersProperties.REFERRER_POLICY_HEADER, config.getReferrerPolicyHeaderValue()); - addHeaderIfEnabled(responseHeaders, headersToAddToResponse, - SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER, config.getContentSecurityPolicyHeaderValue()); + addContentSecurityPolicyHeaderIfEnabled(responseHeaders, headersToAddToResponse, + config.getContentSecurityPolicyHeaderValue()); addHeaderIfEnabled(responseHeaders, headersToAddToResponse, SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER, config.getDownloadOptionsHeaderValue()); @@ -195,6 +195,18 @@ private void addHeaderIfEnabled(HttpHeaders headers, Set headersToAdd, S } } + private void addContentSecurityPolicyHeaderIfEnabled(HttpHeaders headers, Set headersToAdd, + @Nullable String headerValue) { + if (headerValue != null && headersToAdd + .contains(SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER.toLowerCase(Locale.ROOT))) { + if (!headers.containsHeader(SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER) + || !headers.getOrEmpty(SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER) + .contains(headerValue)) { + headers.add(SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER, headerValue); + } + } + } + /** * POJO for {@link SecureHeadersGatewayFilterFactory} filter configuration. */ diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java index e5f2ac761d..356a28dd9d 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java @@ -154,8 +154,8 @@ public void doesNotDuplicateHeaders() { Config config = new Config(); String[] headers = { X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER, X_FRAME_OPTIONS_HEADER, - X_CONTENT_TYPE_OPTIONS_HEADER, REFERRER_POLICY_HEADER, CONTENT_SECURITY_POLICY_HEADER, - X_DOWNLOAD_OPTIONS_HEADER, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, PERMISSIONS_POLICY_HEADER }; + X_CONTENT_TYPE_OPTIONS_HEADER, REFERRER_POLICY_HEADER, X_DOWNLOAD_OPTIONS_HEADER, + X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, PERMISSIONS_POLICY_HEADER }; for (String header : headers) { filter = filterFactory.apply(config); @@ -171,6 +171,25 @@ public void doesNotDuplicateHeaders() { } } + @Test + public void appendsContentSecurityPolicyHeaderWhenAlreadyPresent() { + String originalHeaderValue = "default-src 'self'"; + + SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory( + new SecureHeadersProperties()); + filter = filterFactory.apply(new Config()); + + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build(); + exchange = MockServerWebExchange.from(request); + exchange.getResponse().getHeaders().set(CONTENT_SECURITY_POLICY_HEADER, originalHeaderValue); + + filter.filter(exchange, filterChain).block(); + + ServerHttpResponse response = captor.getValue().getResponse(); + assertThat(response.getHeaders().get(CONTENT_SECURITY_POLICY_HEADER)).containsExactly(originalHeaderValue, + new SecureHeadersProperties().getContentSecurityPolicy()); + } + @Test public void toStringFormat() { GatewayFilter filter = new SecureHeadersGatewayFilterFactory(new SecureHeadersProperties()).apply(new Config());