Adds SecureHeaders filter
see gh-15
This commit is contained in:
@@ -3,7 +3,6 @@ package org.springframework.cloud.gateway.api;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import static org.springframework.util.StringUtils.tokenizeToStringArray;
|
||||
@@ -22,8 +21,8 @@ public class FilterDefinition {
|
||||
public FilterDefinition(String text) {
|
||||
int eqIdx = text.indexOf("=");
|
||||
if (eqIdx <= 0) {
|
||||
throw new ValidationException("Unable to parse FilterDefinition text '" + text + "'" +
|
||||
", must be of the form name=value");
|
||||
setName(text);
|
||||
return;
|
||||
}
|
||||
setName(text.substring(0, eqIdx));
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import org.springframework.cloud.gateway.filter.route.RemoveRequestHeaderRouteFi
|
||||
import org.springframework.cloud.gateway.filter.route.RemoveResponseHeaderRouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.RewritePathRouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.RouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.SecureHeadersProperties;
|
||||
import org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.SetPathRouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.SetResponseHeaderRouteFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.SetStatusRouteFilter;
|
||||
@@ -76,11 +78,6 @@ public class GatewayAutoConfiguration {
|
||||
return new CachingRouteLocator(new PropertiesRouteLocator(properties));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GatewayProperties gatewayProperties() {
|
||||
return new GatewayProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RoutingWebHandler routingWebHandler(HttpClient httpClient) {
|
||||
return new RoutingWebHandler(httpClient);
|
||||
@@ -100,6 +97,18 @@ public class GatewayAutoConfiguration {
|
||||
return new RoutePredicateHandlerMapping(webHandler, predicates, routeLocator);
|
||||
}
|
||||
|
||||
// ConfigurationProperty beans
|
||||
|
||||
@Bean
|
||||
public GatewayProperties gatewayProperties() {
|
||||
return new GatewayProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecureHeadersProperties secureHeadersProperties() {
|
||||
return new SecureHeadersProperties();
|
||||
}
|
||||
|
||||
// GlobalFilter beans
|
||||
|
||||
@Bean
|
||||
@@ -225,6 +234,11 @@ public class GatewayAutoConfiguration {
|
||||
return new SetPathRouteFilter();
|
||||
}
|
||||
|
||||
@Bean(name = "SecureHeadersRouteFilter")
|
||||
public SecureHeadersRouteFilter secureHeadersRouteFilter(SecureHeadersProperties properties) {
|
||||
return new SecureHeadersRouteFilter(properties);
|
||||
}
|
||||
|
||||
@Bean(name = "SetResponseHeaderRouteFilter")
|
||||
public SetResponseHeaderRouteFilter setResponseHeaderRouteFilter() {
|
||||
return new SetResponseHeaderRouteFilter();
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.springframework.cloud.gateway.filter.route;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@ConfigurationProperties("spring.cloud.gateway.filter.secureHeaders")
|
||||
public class SecureHeadersProperties {
|
||||
public static final String X_XSS_PROTECTION_HEADER_DEFAULT = "1; mode=block";
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER_DEFAULT = "max-age=631138519"; //; includeSubDomains preload")
|
||||
public static final String X_FRAME_OPTIONS_HEADER_DEFAULT = "DENY"; //SAMEORIGIN = ALLOW-FROM
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER_DEFAULT = "nosniff";
|
||||
public static final String REFERRER_POLICY_HEADER_DEFAULT = "no-referrer"; //no-referrer-when-downgrade = origin = origin-when-cross-origin = same-origin = strict-origin = strict-origin-when-cross-origin = unsafe-url
|
||||
public static final String CONTENT_SECURITY_POLICY_HEADER_DEFAULT = "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'";
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER_DEFAULT = "noopen";
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER_DEFAULT = "none";
|
||||
|
||||
private String xssProtectionHeader = X_XSS_PROTECTION_HEADER_DEFAULT;
|
||||
private String strictTransportSecurity = STRICT_TRANSPORT_SECURITY_HEADER_DEFAULT;
|
||||
private String frameOptions = X_FRAME_OPTIONS_HEADER_DEFAULT;
|
||||
private String contentTypeOptions = X_CONTENT_TYPE_OPTIONS_HEADER_DEFAULT;
|
||||
private String referrerPolicy = REFERRER_POLICY_HEADER_DEFAULT;
|
||||
private String contentSecurityPolicy = CONTENT_SECURITY_POLICY_HEADER_DEFAULT;
|
||||
private String downloadOptions = X_DOWNLOAD_OPTIONS_HEADER_DEFAULT;
|
||||
private String permittedCrossDomainPolicies = X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER_DEFAULT;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("SecureHeadersProperties{");
|
||||
sb.append("xssProtectionHeader='").append(xssProtectionHeader).append('\'');
|
||||
sb.append(", strictTransportSecurity='").append(strictTransportSecurity).append('\'');
|
||||
sb.append(", frameOptions='").append(frameOptions).append('\'');
|
||||
sb.append(", contentTypeOptions='").append(contentTypeOptions).append('\'');
|
||||
sb.append(", referrerPolicy='").append(referrerPolicy).append('\'');
|
||||
sb.append(", contentSecurityPolicy='").append(contentSecurityPolicy).append('\'');
|
||||
sb.append(", downloadOptions='").append(downloadOptions).append('\'');
|
||||
sb.append(", permittedCrossDomainPolicies='").append(permittedCrossDomainPolicies).append('\'');
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.springframework.cloud.gateway.filter.route;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
/**
|
||||
* https://blog.appcanary.com/2017/http-security-headers.html
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class SecureHeadersRouteFilter implements RouteFilter {
|
||||
|
||||
public static final String X_XSS_PROTECTION_HEADER = "X-Xss-Protection";
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER = "Strict-Transport-Security";
|
||||
public static final String X_FRAME_OPTIONS_HEADER = "X-Frame-Options";
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
|
||||
public static final String REFERRER_POLICY_HEADER = "Referrer-Policy";
|
||||
public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER = "X-Download-Options";
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER = "X-Permitted-Cross-Domain-Policies";
|
||||
|
||||
private final SecureHeadersProperties properties;
|
||||
|
||||
public SecureHeadersRouteFilter(SecureHeadersProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebFilter apply(String... args) {
|
||||
//TODO: allow args to override properties
|
||||
|
||||
return (exchange, chain) -> {
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
|
||||
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());
|
||||
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.springframework.boot.context.embedded.LocalServerPort;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.api.Route;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.route.SecureHeadersProperties;
|
||||
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -26,6 +27,14 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.CONTENT_SECURITY_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.REFERRER_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.STRICT_TRANSPORT_SECURITY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.X_CONTENT_TYPE_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.X_DOWNLOAD_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.X_FRAME_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.route.SecureHeadersRouteFilter.X_XSS_PROTECTION_HEADER;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||
@@ -307,7 +316,34 @@ public class GatewayIntegrationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPathFilterWorks() {
|
||||
public void secureHeadersFilterWorks() {
|
||||
Mono<ClientResponse> result = webClient.get()
|
||||
.uri("/headers")
|
||||
.header("Host", "www.secureheaders.org")
|
||||
.exchange();
|
||||
|
||||
SecureHeadersProperties defaults = new SecureHeadersProperties();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(
|
||||
response -> {
|
||||
assertStatus(response, HttpStatus.OK);
|
||||
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
|
||||
assertThat(httpHeaders.getFirst(X_XSS_PROTECTION_HEADER)).isEqualTo(defaults.getXssProtectionHeader());
|
||||
assertThat(httpHeaders.getFirst(STRICT_TRANSPORT_SECURITY_HEADER)).isEqualTo(defaults.getStrictTransportSecurity());
|
||||
assertThat(httpHeaders.getFirst(X_FRAME_OPTIONS_HEADER)).isEqualTo(defaults.getFrameOptions());
|
||||
assertThat(httpHeaders.getFirst(X_CONTENT_TYPE_OPTIONS_HEADER)).isEqualTo(defaults.getContentTypeOptions());
|
||||
assertThat(httpHeaders.getFirst(REFERRER_POLICY_HEADER)).isEqualTo(defaults.getReferrerPolicy());
|
||||
assertThat(httpHeaders.getFirst(CONTENT_SECURITY_POLICY_HEADER)).isEqualTo(defaults.getContentSecurityPolicy());
|
||||
assertThat(httpHeaders.getFirst(X_DOWNLOAD_OPTIONS_HEADER)).isEqualTo(defaults.getDownloadOptions());
|
||||
assertThat(httpHeaders.getFirst(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)).isEqualTo(defaults.getPermittedCrossDomainPolicies());
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(DURATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPathFilterDefaultValuesWork() {
|
||||
Mono<ClientResponse> result = webClient.get()
|
||||
.uri("/foo/get")
|
||||
.header("Host", "www.setpath.org")
|
||||
|
||||
@@ -97,6 +97,15 @@ spring:
|
||||
- AddResponseHeader=X-Request-Foo, Bar
|
||||
- RemoveResponseHeader=X-Request-Foo
|
||||
|
||||
# =====================================
|
||||
- id: secure_headers_test
|
||||
uri: http://httpbin.org:80
|
||||
predicates:
|
||||
- Host=**.secureheaders.org
|
||||
- Url=/headers
|
||||
filters:
|
||||
- SecureHeaders
|
||||
|
||||
# =====================================
|
||||
- id: set_path_test
|
||||
uri: http://httpbin.org:80
|
||||
@@ -165,8 +174,8 @@ myservice:
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway: TRACE
|
||||
org.springframework.http.server.reactive: DEBUG
|
||||
reactor.ipc.netty: DEBUG
|
||||
# org.springframework.http.server.reactive: DEBUG
|
||||
# reactor.ipc.netty: DEBUG
|
||||
|
||||
management:
|
||||
context-path: /admin
|
||||
|
||||
Reference in New Issue
Block a user