From ee6f7d01c48dec69c618fba6d28e12062e0e13e5 Mon Sep 17 00:00:00 2001 From: Thirunavukkarasu Ravichandran Date: Sat, 4 May 2019 09:59:44 +0200 Subject: [PATCH 1/2] Fixes gh-878 - secure headers can be disabled --- .../main/asciidoc/spring-cloud-gateway.adoc | 6 +- .../SecureHeadersGatewayFilterFactory.java | 58 ++++++--- .../factory/SecureHeadersProperties.java | 16 ++- ...eHeadersGatewayFilterFactoryUnitTests.java | 112 ++++++++++++++++++ 4 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 6eb452e1..7c0a8117 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -790,7 +790,7 @@ spring: If you are integrating https://projects.spring.io/spring-security/[Spring Security] with Spring Session, and want to ensure security details have been forwarded to the remote process, this is critical. === SecureHeaders GatewayFilter Factory -The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the reccomendation from https://blog.appcanary.com/2017/http-security-headers.html[this blog post]. +The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the recommendation from https://blog.appcanary.com/2017/http-security-headers.html[this blog post]. .The following headers are added (allong with default values): * `X-Xss-Protection:1; mode=block` @@ -814,6 +814,10 @@ To change the default values set the appropriate property in the `spring.cloud.g * `download-options` * `permitted-cross-domain-policies` +To disable the default values set the property `spring.cloud.gateway.filter.secure-headers.disable` with comma separated values. + +.Example: +`spring.cloud.gateway.filter.secure-headers.disable=frame-options,download-options` === SetPath GatewayFilter Factory The SetPath GatewayFilter Factory takes a path `template` parameter. It offers a simple way to manipulate the request path by allowing templated segments of the path. This uses the uri templates from Spring Framework. Multiple matching segments are allowed. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java index 608ccd33..0ccf557f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java @@ -16,13 +16,15 @@ package org.springframework.cloud.gateway.filter.factory; +import java.util.List; + import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.http.HttpHeaders; /** * https://blog.appcanary.com/2017/http-security-headers.html. * - * @author Spencer Gibb + * @author Spencer Gibb, Thirunavukkarasu Ravichandran */ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory { @@ -79,22 +81,50 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact return (exchange, chain) -> { HttpHeaders headers = exchange.getResponse().getHeaders(); - // TODO: allow header to be disabled - headers.add(X_XSS_PROTECTION_HEADER, properties.getXssProtectionHeader()); - headers.add(STRICT_TRANSPORT_SECURITY_HEADER, - properties.getStrictTransportSecurity()); - headers.add(X_FRAME_OPTIONS_HEADER, properties.getFrameOptions()); - headers.add(X_CONTENT_TYPE_OPTIONS_HEADER, - properties.getContentTypeOptions()); - headers.add(REFERRER_POLICY_HEADER, properties.getReferrerPolicy()); - headers.add(CONTENT_SECURITY_POLICY_HEADER, - properties.getContentSecurityPolicy()); - headers.add(X_DOWNLOAD_OPTIONS_HEADER, properties.getDownloadOptions()); - headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, - properties.getPermittedCrossDomainPolicies()); + List disabled = properties.getDisable(); + + if (isDisabled(disabled, X_XSS_PROTECTION_HEADER)) { + headers.add(X_XSS_PROTECTION_HEADER, properties.getXssProtectionHeader()); + } + + if (isDisabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) { + headers.add(STRICT_TRANSPORT_SECURITY_HEADER, + properties.getStrictTransportSecurity()); + } + + if (isDisabled(disabled, X_FRAME_OPTIONS_HEADER)) { + headers.add(X_FRAME_OPTIONS_HEADER, properties.getFrameOptions()); + } + + if (isDisabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) { + headers.add(X_CONTENT_TYPE_OPTIONS_HEADER, + properties.getContentTypeOptions()); + } + + if (isDisabled(disabled, REFERRER_POLICY_HEADER)) { + headers.add(REFERRER_POLICY_HEADER, properties.getReferrerPolicy()); + } + + if (isDisabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) { + headers.add(CONTENT_SECURITY_POLICY_HEADER, + properties.getContentSecurityPolicy()); + } + + if (isDisabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) { + headers.add(X_DOWNLOAD_OPTIONS_HEADER, properties.getDownloadOptions()); + } + + if (isDisabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) { + headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, + properties.getPermittedCrossDomainPolicies()); + } return chain.filter(exchange); }; } + private boolean isDisabled(List disabledHeaders, String header) { + return !disabledHeaders.contains(header.toLowerCase()); + } + } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java index 0976a49d..ea553711 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java @@ -16,10 +16,13 @@ package org.springframework.cloud.gateway.filter.factory; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; /** - * @author Spencer Gibb + * @author Spencer Gibb, Thirunavukkarasu Ravichandran */ @ConfigurationProperties("spring.cloud.gateway.filter.secure-headers") public class SecureHeadersProperties { @@ -95,6 +98,8 @@ public class SecureHeadersProperties { private String permittedCrossDomainPolicies = X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER_DEFAULT; + private List disable = new ArrayList<>(); + public String getXssProtectionHeader() { return xssProtectionHeader; } @@ -159,6 +164,14 @@ public class SecureHeadersProperties { this.permittedCrossDomainPolicies = permittedCrossDomainPolicies; } + public List getDisable() { + return disable; + } + + public void setDisable(List disable) { + this.disable = disable; + } + @Override public String toString() { final StringBuffer sb = new StringBuffer("SecureHeadersProperties{"); @@ -172,6 +185,7 @@ public class SecureHeadersProperties { sb.append(", downloadOptions='").append(downloadOptions).append('\''); sb.append(", permittedCrossDomainPolicies='").append(permittedCrossDomainPolicies) .append('\''); + sb.append(", disabled='").append(disable).append('\''); sb.append('}'); return sb.toString(); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java new file mode 100644 index 00000000..5363737e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.CONTENT_SECURITY_POLICY_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.REFERRER_POLICY_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.STRICT_TRANSPORT_SECURITY_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.X_CONTENT_TYPE_OPTIONS_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.X_DOWNLOAD_OPTIONS_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.X_FRAME_OPTIONS_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER; +import static org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory.X_XSS_PROTECTION_HEADER; + +/** + * @author Thirunavukkarasu Ravichandran + */ +public class SecureHeadersGatewayFilterFactoryUnitTests { + + private GatewayFilter filter; + + private ServerWebExchange exchange; + + private GatewayFilterChain filterChain; + + private ArgumentCaptor captor; + + @Before + public void setUp() { + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost") + .build(); + exchange = MockServerWebExchange.from(request); + filterChain = mock(GatewayFilterChain.class); + captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + + } + + @Test + public void addAllHeadersIfNothingIsDisabled() { + SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory( + new SecureHeadersProperties()); + NameConfig config = new NameConfig(); + config.setName("SecureHeadersGatewayFilter"); + filter = filterFactory.apply(config); + + filter.filter(exchange, filterChain); + + ServerHttpResponse response = captor.getValue().getResponse(); + assertThat(response.getHeaders()).containsKeys(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); + } + + @Test + public void doNotAddDisabledHeaders() { + SecureHeadersProperties properties = new SecureHeadersProperties(); + properties.setDisable(asList("x-xss-protection", "strict-transport-security", + "x-frame-options", "x-content-type-options", "referrer-policy", + "content-security-policy", "x-download-options", + "x-permitted-cross-domain-policies")); + + SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory( + properties); + NameConfig config = new NameConfig(); + config.setName("SecureHeadersGatewayFilter"); + filter = filterFactory.apply(config); + + filter.filter(exchange, filterChain); + + ServerHttpResponse response = captor.getValue().getResponse(); + assertThat(response.getHeaders()).doesNotContainKeys(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); + + } + +} From dbcd9bac5df67218a33c961c5cb5c4e4b6c1fcf5 Mon Sep 17 00:00:00 2001 From: Thirunavukkarasu Ravichandran Date: Tue, 4 Jun 2019 06:59:38 +0200 Subject: [PATCH 2/2] renaming the method --- .../SecureHeadersGatewayFilterFactory.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java index 0ccf557f..0d0e3da5 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java @@ -83,38 +83,38 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact List disabled = properties.getDisable(); - if (isDisabled(disabled, X_XSS_PROTECTION_HEADER)) { + if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) { headers.add(X_XSS_PROTECTION_HEADER, properties.getXssProtectionHeader()); } - if (isDisabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) { + if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) { headers.add(STRICT_TRANSPORT_SECURITY_HEADER, properties.getStrictTransportSecurity()); } - if (isDisabled(disabled, X_FRAME_OPTIONS_HEADER)) { + if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) { headers.add(X_FRAME_OPTIONS_HEADER, properties.getFrameOptions()); } - if (isDisabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) { + if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) { headers.add(X_CONTENT_TYPE_OPTIONS_HEADER, properties.getContentTypeOptions()); } - if (isDisabled(disabled, REFERRER_POLICY_HEADER)) { + if (isEnabled(disabled, REFERRER_POLICY_HEADER)) { headers.add(REFERRER_POLICY_HEADER, properties.getReferrerPolicy()); } - if (isDisabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) { + if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) { headers.add(CONTENT_SECURITY_POLICY_HEADER, properties.getContentSecurityPolicy()); } - if (isDisabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) { + if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) { headers.add(X_DOWNLOAD_OPTIONS_HEADER, properties.getDownloadOptions()); } - if (isDisabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) { + if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) { headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, properties.getPermittedCrossDomainPolicies()); } @@ -123,7 +123,7 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact }; } - private boolean isDisabled(List disabledHeaders, String header) { + private boolean isEnabled(List disabledHeaders, String header) { return !disabledHeaders.contains(header.toLowerCase()); }