diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java index 3c9e7dff..1a05a8b9 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactory.java @@ -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 { /** * 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 filter(ServerWebExchange exchange, @@ -91,43 +90,38 @@ public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFact HttpHeaders headers = exchange.getResponse().getHeaders(); List 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; + } + + } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index d990f46c..426e404c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -565,10 +565,19 @@ public class GatewayFilterSpec extends UriSpec { * post. * @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 + * this blog + * post. + * @param configConsumer self define headers + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec secureHeaders(Consumer configConsumer) { + return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(configConsumer)); } /** diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java index b6699aad..4e3fe555 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryUnitTests.java @@ -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"); }