Allows SecureHeaders filter header values to be set per route.
Fixes gh-2005 Fixes gh-2008
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user