Merge pull request #1042 from thirunar/2.1.x
Fixes gh-878 - secure headers can be disabled #1041
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<String> disabled = properties.getDisable();
|
||||
|
||||
if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) {
|
||||
headers.add(X_XSS_PROTECTION_HEADER, properties.getXssProtectionHeader());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) {
|
||||
headers.add(STRICT_TRANSPORT_SECURITY_HEADER,
|
||||
properties.getStrictTransportSecurity());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) {
|
||||
headers.add(X_FRAME_OPTIONS_HEADER, properties.getFrameOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) {
|
||||
headers.add(X_CONTENT_TYPE_OPTIONS_HEADER,
|
||||
properties.getContentTypeOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, REFERRER_POLICY_HEADER)) {
|
||||
headers.add(REFERRER_POLICY_HEADER, properties.getReferrerPolicy());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) {
|
||||
headers.add(CONTENT_SECURITY_POLICY_HEADER,
|
||||
properties.getContentSecurityPolicy());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) {
|
||||
headers.add(X_DOWNLOAD_OPTIONS_HEADER, properties.getDownloadOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) {
|
||||
headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER,
|
||||
properties.getPermittedCrossDomainPolicies());
|
||||
}
|
||||
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isEnabled(List<String> disabledHeaders, String header) {
|
||||
return !disabledHeaders.contains(header.toLowerCase());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> disable = new ArrayList<>();
|
||||
|
||||
public String getXssProtectionHeader() {
|
||||
return xssProtectionHeader;
|
||||
}
|
||||
@@ -159,6 +164,14 @@ public class SecureHeadersProperties {
|
||||
this.permittedCrossDomainPolicies = permittedCrossDomainPolicies;
|
||||
}
|
||||
|
||||
public List<String> getDisable() {
|
||||
return disable;
|
||||
}
|
||||
|
||||
public void setDisable(List<String> 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();
|
||||
}
|
||||
|
||||
@@ -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<ServerWebExchange> 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user