Allows SecureHeaders filter header values to be set per route.

Fixes gh-2005
Fixes gh-2008
This commit is contained in:
ctlove0523
2020-10-28 22:27:33 +08:00
committed by spencergibb
parent 4c8a1d6051
commit fffcd23214
3 changed files with 187 additions and 32 deletions

View File

@@ -32,7 +32,7 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.fi
*
* @author Spencer Gibb, Thirunavukkarasu Ravichandran
*/
public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory {
public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory<SecureHeadersGatewayFilterFactory.Config> {
/**
* Xss-Protection header name.
@@ -77,13 +77,12 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact
private final SecureHeadersProperties properties;
public SecureHeadersGatewayFilterFactory(SecureHeadersProperties properties) {
super(Config.class);
this.properties = properties;
}
@Override
public GatewayFilter apply(Object config) {
// TODO: allow args to override properties
public GatewayFilter apply(Config originalConfig) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
@@ -91,43 +90,38 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact
HttpHeaders headers = exchange.getResponse().getHeaders();
List<String> disabled = properties.getDisable();
Config config = originalConfig.withDefaults(properties);
if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) {
headers.add(X_XSS_PROTECTION_HEADER,
properties.getXssProtectionHeader());
headers.add(X_XSS_PROTECTION_HEADER, config.getXssProtectionHeader());
}
if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) {
headers.add(STRICT_TRANSPORT_SECURITY_HEADER,
properties.getStrictTransportSecurity());
headers.add(STRICT_TRANSPORT_SECURITY_HEADER, config.getStrictTransportSecurity());
}
if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) {
headers.add(X_FRAME_OPTIONS_HEADER, properties.getFrameOptions());
headers.add(X_FRAME_OPTIONS_HEADER, config.getFrameOptions());
}
if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) {
headers.add(X_CONTENT_TYPE_OPTIONS_HEADER,
properties.getContentTypeOptions());
headers.add(X_CONTENT_TYPE_OPTIONS_HEADER, config.getContentTypeOptions());
}
if (isEnabled(disabled, REFERRER_POLICY_HEADER)) {
headers.add(REFERRER_POLICY_HEADER, properties.getReferrerPolicy());
headers.add(REFERRER_POLICY_HEADER, config.getReferrerPolicy());
}
if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) {
headers.add(CONTENT_SECURITY_POLICY_HEADER,
properties.getContentSecurityPolicy());
headers.add(CONTENT_SECURITY_POLICY_HEADER, config.getContentSecurityPolicy());
}
if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) {
headers.add(X_DOWNLOAD_OPTIONS_HEADER,
properties.getDownloadOptions());
headers.add(X_DOWNLOAD_OPTIONS_HEADER, config.getDownloadOptions());
}
if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) {
headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER,
properties.getPermittedCrossDomainPolicies());
headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, config.getPermittedCrossDomainPolicies());
}
return chain.filter(exchange);
@@ -145,4 +139,132 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact
return !disabledHeaders.contains(header.toLowerCase());
}
public static class Config {
private String xssProtectionHeader;
private String strictTransportSecurity;
private String frameOptions;
private String contentTypeOptions;
private String referrerPolicy;
private String contentSecurityPolicy;
private String downloadOptions;
private String permittedCrossDomainPolicies;
public Config withDefaults(SecureHeadersProperties properties) {
Config config = new Config();
config.setXssProtectionHeader(xssProtectionHeader);
config.setStrictTransportSecurity(strictTransportSecurity);
config.setFrameOptions(frameOptions);
config.setContentTypeOptions(contentTypeOptions);
config.setReferrerPolicy(referrerPolicy);
config.setContentSecurityPolicy(contentSecurityPolicy);
config.setDownloadOptions(downloadOptions);
config.setPermittedCrossDomainPolicies(permittedCrossDomainPolicies);
if (config.xssProtectionHeader == null) {
config.xssProtectionHeader = properties.getXssProtectionHeader();
}
if (config.strictTransportSecurity == null) {
config.strictTransportSecurity = properties.getStrictTransportSecurity();
}
if (config.frameOptions == null) {
config.frameOptions = properties.getFrameOptions();
}
if (config.contentTypeOptions == null) {
config.contentTypeOptions = properties.getContentTypeOptions();
}
if (config.referrerPolicy == null) {
config.referrerPolicy = properties.getReferrerPolicy();
}
if (config.contentSecurityPolicy == null) {
config.contentSecurityPolicy = properties.getContentSecurityPolicy();
}
if (config.downloadOptions == null) {
config.downloadOptions = properties.getDownloadOptions();
}
if (config.permittedCrossDomainPolicies == null) {
config.permittedCrossDomainPolicies = properties.getPermittedCrossDomainPolicies();
}
return config;
}
public String getXssProtectionHeader() {
return xssProtectionHeader;
}
public void setXssProtectionHeader(String xssProtectionHeader) {
this.xssProtectionHeader = xssProtectionHeader;
}
public String getStrictTransportSecurity() {
return strictTransportSecurity;
}
public void setStrictTransportSecurity(String strictTransportSecurity) {
this.strictTransportSecurity = strictTransportSecurity;
}
public String getFrameOptions() {
return frameOptions;
}
public void setFrameOptions(String frameOptions) {
this.frameOptions = frameOptions;
}
public String getContentTypeOptions() {
return contentTypeOptions;
}
public void setContentTypeOptions(String contentTypeOptions) {
this.contentTypeOptions = contentTypeOptions;
}
public String getReferrerPolicy() {
return referrerPolicy;
}
public void setReferrerPolicy(String referrerPolicy) {
this.referrerPolicy = referrerPolicy;
}
public String getContentSecurityPolicy() {
return contentSecurityPolicy;
}
public void setContentSecurityPolicy(String contentSecurityPolicy) {
this.contentSecurityPolicy = contentSecurityPolicy;
}
public String getDownloadOptions() {
return downloadOptions;
}
public void setDownloadOptions(String downloadOptions) {
this.downloadOptions = downloadOptions;
}
public String getPermittedCrossDomainPolicies() {
return permittedCrossDomainPolicies;
}
public void setPermittedCrossDomainPolicies(String permittedCrossDomainPolicies) {
this.permittedCrossDomainPolicies = permittedCrossDomainPolicies;
}
}
}

View File

@@ -565,10 +565,19 @@ public class GatewayFilterSpec extends UriSpec {
* post</a>.
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
*/
@SuppressWarnings("unchecked")
public GatewayFilterSpec secureHeaders() {
return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(c -> {
}));
return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(config -> {}));
}
/**
* A filter that adds a number of headers to the response at the reccomendation from
* <a href="https://blog.appcanary.com/2017/http-security-headers.html">this blog
* post</a>.
* @param configConsumer self define headers
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
*/
public GatewayFilterSpec secureHeaders(Consumer<SecureHeadersGatewayFilterFactory.Config> configConsumer) {
return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(configConsumer));
}
/**

View File

@@ -24,7 +24,6 @@ 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;
@@ -35,6 +34,7 @@ import static org.assertj.core.api.Assertions.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.Config;
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;
@@ -71,9 +71,7 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
public void addAllHeadersIfNothingIsDisabled() {
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(
new SecureHeadersProperties());
NameConfig config = new NameConfig();
config.setName("SecureHeadersGatewayFilter");
filter = filterFactory.apply(config);
filter = filterFactory.apply(new Config());
filter.filter(exchange, filterChain);
@@ -93,11 +91,8 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
"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);
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
filter = filterFactory.apply(new Config());
filter.filter(exchange, filterChain);
@@ -110,10 +105,39 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
}
@Test
public void overrideSomeHeaders() {
SecureHeadersProperties properties = new SecureHeadersProperties();
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(
new SecureHeadersProperties());
Config config = new Config();
config.setStrictTransportSecurity("max-age=65535");
config.setReferrerPolicy("referrer");
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);
assertThat(response.getHeaders().get(STRICT_TRANSPORT_SECURITY_HEADER)).containsOnly("max-age=65535");
assertThat(response.getHeaders().get(REFERRER_POLICY_HEADER)).containsOnly("referrer");
assertThat(response.getHeaders().get(X_XSS_PROTECTION_HEADER)).containsOnly(properties.getXssProtectionHeader());
assertThat(response.getHeaders().get(X_FRAME_OPTIONS_HEADER)).containsOnly(properties.getFrameOptions());
assertThat(response.getHeaders().get(X_CONTENT_TYPE_OPTIONS_HEADER)).containsOnly(properties.getContentTypeOptions());
assertThat(response.getHeaders().get(CONTENT_SECURITY_POLICY_HEADER)).containsOnly(properties.getContentSecurityPolicy());
assertThat(response.getHeaders().get(X_DOWNLOAD_OPTIONS_HEADER)).containsOnly(properties.getDownloadOptions());
assertThat(response.getHeaders().get(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)).containsOnly(properties.getPermittedCrossDomainPolicies());
}
@Test
public void toStringFormat() {
GatewayFilter filter = new SecureHeadersGatewayFilterFactory(
new SecureHeadersProperties()).apply("");
GatewayFilter filter = new SecureHeadersGatewayFilterFactory(new SecureHeadersProperties()).apply(new Config());
Assertions.assertThat(filter.toString()).contains("SecureHeaders");
}