#2975-Suggestion-Add-Permissions-Policy-as-configurable-option-to-SecureHeaders-GatewayFilter
- added Permissions-Policy header as an opt-in header & default value - updated documentation with Permissions-Policy and resources - updated tests to include Permissions-Policy - SecurityHeaders and Permissions-Policy may be configured globally / per route - updated structure of public methods and class members to match previous version structure Fixes gh-2975 Signed-off-by: Jörg Richter <96986086+joerg-richter-5234@users.noreply.github.com>
This commit is contained in:
@@ -29,10 +29,87 @@ The following properties are available:
|
||||
To disable the default values set the `spring.cloud.gateway.filter.secure-headers.disable` property with comma-separated values.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source]
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
filter:
|
||||
secure-headers:
|
||||
disable: x-frame-options,strict-transport-security
|
||||
----
|
||||
|
||||
NOTE: The lowercase full name of the secure header needs to be used to disable it..
|
||||
To apply the `SecureHeaders` filter to a specific route, add the filter to the list of filters of that route.
|
||||
You can customize the route filter using arguments. Route configuration overrides the global default configuration for this route.
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
- id: secureheaders_route
|
||||
uri: http://example.org
|
||||
predicates:
|
||||
- Path=/**
|
||||
filters:
|
||||
- name: SecureHeaders
|
||||
args:
|
||||
disable: x-frame-options
|
||||
----
|
||||
|
||||
NOTE: The lowercase full name of the secure header needs to be used to disable it.
|
||||
|
||||
== Further options
|
||||
|
||||
You may opt in to add the `Permissions-Policy` header to the response. Permissions Policy is a security header
|
||||
that allows web developers to manage which browser features a website can utilize. Please see
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy[Permissions-Policy] and
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives[Directives] to configure it
|
||||
for your environment.
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
filter:
|
||||
secure-headers:
|
||||
enable: permissions-policy
|
||||
permissions-policy : geolocation=(self "https://example.com")
|
||||
----
|
||||
|
||||
In the above https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/geolocation[example]
|
||||
the Geolocation API is disabled within all browsing contexts except for its own origin and those whose origin is "https://example.com".
|
||||
The Permissions-Policy may be configured separately for each route.
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
- id: secureheaders_route
|
||||
uri: http://anotherexample.org
|
||||
predicates:
|
||||
- Path=/**
|
||||
filters:
|
||||
- name: SecureHeaders
|
||||
args:
|
||||
disable: x-frame-options
|
||||
enable: permissions-policy
|
||||
permissions-policy : geolocation=("https://anotherexample.org")
|
||||
----
|
||||
|
||||
WARNING: When you enable Permissions-Policy and do not explicitly configure any directives, a default value will be applied.
|
||||
Specifically, this default value disables a wide range of standardized and experimental features.
|
||||
This behavior might not be appropriate for your specific environment or use case.
|
||||
|
||||
Permissions-Policy default value when enabled and no explicit configuration:
|
||||
|
||||
`Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(),
|
||||
display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(),
|
||||
fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(),
|
||||
payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(),
|
||||
web-share=(), xr-spatial-tracking=()`
|
||||
|
||||
|
||||
NOTE: You can check the Permissions Policy feature list for Chrome with https://developer.chrome.com/docs/privacy-security/permissions-policy#chrome_devtools_integration[DevTool Integration].
|
||||
|
||||
When you configure the header value for your environment, make sure to check the browser console for syntax errors.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
test:
|
||||
hostport: httpbin.org:80
|
||||
# hostport: localhost:5000
|
||||
uri: http://${test.hostport}
|
||||
#uri: lb://httpbin
|
||||
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
filter:
|
||||
default-filters:
|
||||
#- PrefixPath=/httpbin
|
||||
#- AddResponseHeader=X-Response-Default-Foo, Default-Bar
|
||||
|
||||
routes:
|
||||
# =====================================
|
||||
# to run server
|
||||
# $ wscat --listen 9000
|
||||
# to run client
|
||||
# $ wscat --connect ws://localhost:8080/echo
|
||||
- id: websocket_test
|
||||
uri: ws://localhost:9000
|
||||
order: 9000
|
||||
predicates:
|
||||
- Path=/echo
|
||||
# =====================================
|
||||
- id: default_path_to_httpbin_secureheaders
|
||||
uri: ${test.uri}
|
||||
order: 10000
|
||||
predicates:
|
||||
- Path=/**
|
||||
filters:
|
||||
- name: SecureHeaders
|
||||
args:
|
||||
disable: x-frame-options
|
||||
enable: permissions-policy
|
||||
permissions-policy : geolocation=("https://example.net")
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway: TRACE
|
||||
org.springframework.http.server.reactive: DEBUG
|
||||
org.springframework.web.reactive: DEBUG
|
||||
reactor.ipc.netty: DEBUG
|
||||
reactor.netty: DEBUG
|
||||
|
||||
management.endpoints.web.exposure.include: '*'
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -29,9 +31,14 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
|
||||
/**
|
||||
* https://blog.appcanary.com/2017/http-security-headers.html.
|
||||
* GatewayFilterFactory to provide a route filter that applies security headers to the
|
||||
* HTTP response. External configuration {@link SecureHeadersProperties} provides
|
||||
* opinionated defaults. Following the recommendations made in <a href=
|
||||
* "https://blog.appcanary.com/2017/http-security-headers.html">Http-Security-Headers</a>.
|
||||
* When opt-out headers are not disabled or explicitly configured, sensible defaults are
|
||||
* applied. Additionally, opt-in headers, such as Permissions-Policy, may be applied.
|
||||
*
|
||||
* @author Spencer Gibb, Thirunavukkarasu Ravichandran
|
||||
* @author Spencer Gibb, Thirunavukkarasu Ravichandran, Jörg Richter
|
||||
*/
|
||||
public class SecureHeadersGatewayFilterFactory
|
||||
extends AbstractGatewayFilterFactory<SecureHeadersGatewayFilterFactory.Config> {
|
||||
@@ -39,42 +46,43 @@ public class SecureHeadersGatewayFilterFactory
|
||||
/**
|
||||
* Xss-Protection header name.
|
||||
*/
|
||||
public static final String X_XSS_PROTECTION_HEADER = "X-Xss-Protection";
|
||||
public static final String X_XSS_PROTECTION_HEADER = SecureHeadersProperties.X_XSS_PROTECTION_HEADER;
|
||||
|
||||
/**
|
||||
* Strict transport security header name.
|
||||
*/
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER = "Strict-Transport-Security";
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER = SecureHeadersProperties.STRICT_TRANSPORT_SECURITY_HEADER;
|
||||
|
||||
/**
|
||||
* Frame options header name.
|
||||
*/
|
||||
public static final String X_FRAME_OPTIONS_HEADER = "X-Frame-Options";
|
||||
public static final String X_FRAME_OPTIONS_HEADER = SecureHeadersProperties.X_FRAME_OPTIONS_HEADER;
|
||||
|
||||
/**
|
||||
* Content-Type Options header name.
|
||||
*/
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER = SecureHeadersProperties.X_CONTENT_TYPE_OPTIONS_HEADER;
|
||||
|
||||
/**
|
||||
* Referrer Policy header name.
|
||||
*/
|
||||
public static final String REFERRER_POLICY_HEADER = "Referrer-Policy";
|
||||
public static final String REFERRER_POLICY_HEADER = SecureHeadersProperties.REFERRER_POLICY_HEADER;
|
||||
|
||||
/**
|
||||
* Content-Security Policy header name.
|
||||
*/
|
||||
public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
|
||||
public static final String CONTENT_SECURITY_POLICY_HEADER = SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER;
|
||||
|
||||
/**
|
||||
* Download Options header name.
|
||||
*/
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER = "X-Download-Options";
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER = SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER;
|
||||
|
||||
/**
|
||||
* Permitted Cross-Domain Policies header name.
|
||||
*/
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER = "X-Permitted-Cross-Domain-Policies";
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER = SecureHeadersProperties.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER;
|
||||
|
||||
|
||||
private final SecureHeadersProperties properties;
|
||||
|
||||
@@ -83,50 +91,23 @@ public class SecureHeadersGatewayFilterFactory
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a GatewayFilter that applies security headers to the HTTP response.
|
||||
* @param originalConfig the original security configuration
|
||||
* @return a GatewayFilter instance that applies security headers to the HTTP response
|
||||
*/
|
||||
@Override
|
||||
public GatewayFilter apply(Config originalConfig) {
|
||||
return new GatewayFilter() {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
|
||||
List<String> disabled = properties.getDisable();
|
||||
HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
|
||||
Set<String> headersToAddToResponse = assembleHeaders(originalConfig, properties);
|
||||
|
||||
Config config = originalConfig.withDefaults(properties);
|
||||
|
||||
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
|
||||
if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) {
|
||||
headers.addIfAbsent(X_XSS_PROTECTION_HEADER, config.getXssProtectionHeader());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) {
|
||||
headers.addIfAbsent(STRICT_TRANSPORT_SECURITY_HEADER, config.getStrictTransportSecurity());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) {
|
||||
headers.addIfAbsent(X_FRAME_OPTIONS_HEADER, config.getFrameOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) {
|
||||
headers.addIfAbsent(X_CONTENT_TYPE_OPTIONS_HEADER, config.getContentTypeOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, REFERRER_POLICY_HEADER)) {
|
||||
headers.addIfAbsent(REFERRER_POLICY_HEADER, config.getReferrerPolicy());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) {
|
||||
headers.addIfAbsent(CONTENT_SECURITY_POLICY_HEADER, config.getContentSecurityPolicy());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) {
|
||||
headers.addIfAbsent(X_DOWNLOAD_OPTIONS_HEADER, config.getDownloadOptions());
|
||||
}
|
||||
|
||||
if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) {
|
||||
headers.addIfAbsent(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER,
|
||||
config.getPermittedCrossDomainPolicies());
|
||||
}
|
||||
}));
|
||||
return chain.filter(exchange).then(Mono.fromRunnable(() ->
|
||||
applySecurityHeaders(responseHeaders, headersToAddToResponse, config)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,135 +117,299 @@ public class SecureHeadersGatewayFilterFactory
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isEnabled(List<String> disabledHeaders, String header) {
|
||||
return !disabledHeaders.contains(header.toLowerCase(Locale.ROOT));
|
||||
/**
|
||||
* Applies security headers to the response using the given filter configuration.
|
||||
* @param responseHeaders - the http headers of the response
|
||||
* @param headersToAddToResponse - the security headers that are to be added to the response
|
||||
* @param config - the security filter configuration
|
||||
*/
|
||||
private void applySecurityHeaders(HttpHeaders responseHeaders, Set<String> headersToAddToResponse, Config config) {
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.X_XSS_PROTECTION_HEADER, config.getXssProtectionHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.STRICT_TRANSPORT_SECURITY_HEADER,
|
||||
config.getStrictTransportSecurityHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.X_FRAME_OPTIONS_HEADER,
|
||||
config.getFrameOptionsHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.X_CONTENT_TYPE_OPTIONS_HEADER,
|
||||
config.getContentTypeOptionsHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.REFERRER_POLICY_HEADER,
|
||||
config.getReferrerPolicyHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER,
|
||||
config.getContentSecurityPolicyHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER,
|
||||
config.getDownloadOptionsHeaderValue());
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER,
|
||||
config.getPermittedCrossDomainPoliciesHeaderValue());
|
||||
|
||||
String permissionPolicyHeaderValue = config.getPermissionPolicyHeaderValue();
|
||||
if (config.isRouteFilterConfigProvided()) {
|
||||
String routePermissionPolicyHeaderValue = config.getRoutePermissionsPolicyHeaderValue();
|
||||
if (routePermissionPolicyHeaderValue != null) {
|
||||
permissionPolicyHeaderValue = routePermissionPolicyHeaderValue;
|
||||
}
|
||||
}
|
||||
|
||||
addHeaderIfEnabled(responseHeaders, headersToAddToResponse,
|
||||
SecureHeadersProperties.PERMISSIONS_POLICY_HEADER,
|
||||
permissionPolicyHeaderValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the set of security headers that are to be applied to the response
|
||||
* - When route specific arguments are set, route specific headers are applied.
|
||||
* - When no route specific arguments are set, global default headers are applied.
|
||||
* @param config - the global / route configuration supplied
|
||||
* @param properties - default security headers configuration provided
|
||||
* @return set of security headers that are to be added to the response
|
||||
*/
|
||||
private Set<String> assembleHeaders(Config config, SecureHeadersProperties properties) {
|
||||
Set<String> headersToAddToResponse = new HashSet<>(properties.getDefaultHeaders());
|
||||
if (config.isRouteFilterConfigProvided()) {
|
||||
headersToAddToResponse.addAll(config.getRouteEnabledHeaders());
|
||||
headersToAddToResponse.removeAll(config.getRouteDisabledHeaders());
|
||||
}
|
||||
else {
|
||||
headersToAddToResponse.addAll(properties.getEnabledHeaders());
|
||||
headersToAddToResponse.removeAll(properties.getDisabledHeaders());
|
||||
}
|
||||
return headersToAddToResponse;
|
||||
}
|
||||
|
||||
|
||||
private void addHeaderIfEnabled(HttpHeaders headers, Set<String> headersToAdd, String headerName, String headerValue) {
|
||||
if (headersToAdd.contains(headerName.toLowerCase(Locale.ROOT))) {
|
||||
headers.addIfAbsent(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POJO for {@link SecureHeadersGatewayFilterFactory} filter configuration.
|
||||
*/
|
||||
public static class Config {
|
||||
|
||||
private String xssProtectionHeader;
|
||||
private Set<String> routeEnabledHeaders = new HashSet<>();
|
||||
|
||||
private String strictTransportSecurity;
|
||||
private Set<String> routeDisabledHeaders = new HashSet<>();
|
||||
|
||||
private String frameOptions;
|
||||
private String routePermissionsPolicyHeaderValue;
|
||||
|
||||
private String contentTypeOptions;
|
||||
private boolean routeFilterConfigProvided;
|
||||
|
||||
private String referrerPolicy;
|
||||
private String xssProtectionHeaderValue;
|
||||
|
||||
private String contentSecurityPolicy;
|
||||
private String strictTransportSecurityHeaderValue;
|
||||
|
||||
private String downloadOptions;
|
||||
private String frameOptionsHeaderValue;
|
||||
|
||||
private String permittedCrossDomainPolicies;
|
||||
private String contentTypeOptionsHeaderValue;
|
||||
|
||||
private String referrerPolicyHeaderValue;
|
||||
|
||||
private String contentSecurityPolicyHeaderValue;
|
||||
|
||||
private String downloadOptionsHeaderValue;
|
||||
|
||||
private String permittedCrossDomainPoliciesHeaderValue;
|
||||
|
||||
private String permissionPolicyHeaderValue;
|
||||
|
||||
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();
|
||||
config.setEnable(routeEnabledHeaders);
|
||||
config.setDisable(routeDisabledHeaders);
|
||||
config.setPermissionsPolicy(routePermissionsPolicyHeaderValue);
|
||||
|
||||
config.setXssProtectionHeaderValue(xssProtectionHeaderValue);
|
||||
config.setStrictTransportSecurityHeaderValue(strictTransportSecurityHeaderValue);
|
||||
config.setFrameOptionsHeaderValue(frameOptionsHeaderValue);
|
||||
config.setContentTypeOptionsHeaderValue(contentTypeOptionsHeaderValue);
|
||||
config.setReferrerPolicyHeaderValue(referrerPolicyHeaderValue);
|
||||
config.setContentSecurityPolicyHeaderValue(contentSecurityPolicyHeaderValue);
|
||||
config.setDownloadOptionsHeaderValue(downloadOptionsHeaderValue);
|
||||
config.setPermittedCrossDomainPoliciesHeaderValue(permittedCrossDomainPoliciesHeaderValue);
|
||||
config.setPermissionPolicyHeaderValue(permissionPolicyHeaderValue);
|
||||
|
||||
if (config.xssProtectionHeaderValue == null) {
|
||||
config.xssProtectionHeaderValue = properties.getXssProtectionHeader();
|
||||
}
|
||||
|
||||
if (config.strictTransportSecurity == null) {
|
||||
config.strictTransportSecurity = properties.getStrictTransportSecurity();
|
||||
if (config.strictTransportSecurityHeaderValue == null) {
|
||||
config.strictTransportSecurityHeaderValue = properties.getStrictTransportSecurity();
|
||||
}
|
||||
|
||||
if (config.frameOptions == null) {
|
||||
config.frameOptions = properties.getFrameOptions();
|
||||
if (config.frameOptionsHeaderValue == null) {
|
||||
config.frameOptionsHeaderValue = properties.getFrameOptions();
|
||||
}
|
||||
|
||||
if (config.contentTypeOptions == null) {
|
||||
config.contentTypeOptions = properties.getContentTypeOptions();
|
||||
if (config.contentTypeOptionsHeaderValue == null) {
|
||||
config.contentTypeOptionsHeaderValue = properties.getContentTypeOptions();
|
||||
}
|
||||
|
||||
if (config.referrerPolicy == null) {
|
||||
config.referrerPolicy = properties.getReferrerPolicy();
|
||||
if (config.referrerPolicyHeaderValue == null) {
|
||||
config.referrerPolicyHeaderValue = properties.getReferrerPolicy();
|
||||
}
|
||||
|
||||
if (config.contentSecurityPolicy == null) {
|
||||
config.contentSecurityPolicy = properties.getContentSecurityPolicy();
|
||||
if (config.contentSecurityPolicyHeaderValue == null) {
|
||||
config.contentSecurityPolicyHeaderValue = properties.getContentSecurityPolicy();
|
||||
}
|
||||
|
||||
if (config.downloadOptions == null) {
|
||||
config.downloadOptions = properties.getDownloadOptions();
|
||||
if (config.downloadOptionsHeaderValue == null) {
|
||||
config.downloadOptionsHeaderValue = properties.getDownloadOptions();
|
||||
}
|
||||
|
||||
if (config.permittedCrossDomainPolicies == null) {
|
||||
config.permittedCrossDomainPolicies = properties.getPermittedCrossDomainPolicies();
|
||||
if (config.permittedCrossDomainPoliciesHeaderValue == null) {
|
||||
config.permittedCrossDomainPoliciesHeaderValue = properties.getPermittedCrossDomainPolicies();
|
||||
}
|
||||
|
||||
if (config.permissionPolicyHeaderValue == null) {
|
||||
config.permissionPolicyHeaderValue = properties.getPermissionsPolicy();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public String getXssProtectionHeader() {
|
||||
return xssProtectionHeader;
|
||||
public String getXssProtectionHeaderValue() {
|
||||
return xssProtectionHeaderValue;
|
||||
}
|
||||
|
||||
public void setXssProtectionHeader(String xssProtectionHeader) {
|
||||
this.xssProtectionHeader = xssProtectionHeader;
|
||||
public void setXssProtectionHeaderValue(String xssProtectionHeaderHeaderValue) {
|
||||
this.xssProtectionHeaderValue = xssProtectionHeaderHeaderValue;
|
||||
}
|
||||
|
||||
public String getStrictTransportSecurity() {
|
||||
return strictTransportSecurity;
|
||||
public String getStrictTransportSecurityHeaderValue() {
|
||||
return strictTransportSecurityHeaderValue;
|
||||
}
|
||||
|
||||
public void setStrictTransportSecurity(String strictTransportSecurity) {
|
||||
this.strictTransportSecurity = strictTransportSecurity;
|
||||
public void setStrictTransportSecurityHeaderValue(String strictTransportSecurityHeaderValue) {
|
||||
this.strictTransportSecurityHeaderValue = strictTransportSecurityHeaderValue;
|
||||
}
|
||||
|
||||
public String getFrameOptions() {
|
||||
return frameOptions;
|
||||
public String getFrameOptionsHeaderValue() {
|
||||
return frameOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public void setFrameOptions(String frameOptions) {
|
||||
this.frameOptions = frameOptions;
|
||||
public void setFrameOptionsHeaderValue(String frameOptionsHeaderValue) {
|
||||
this.frameOptionsHeaderValue = frameOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public String getContentTypeOptions() {
|
||||
return contentTypeOptions;
|
||||
public String getContentTypeOptionsHeaderValue() {
|
||||
return contentTypeOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public void setContentTypeOptions(String contentTypeOptions) {
|
||||
this.contentTypeOptions = contentTypeOptions;
|
||||
public void setContentTypeOptionsHeaderValue(String contentTypeOptionsHeaderValue) {
|
||||
this.contentTypeOptionsHeaderValue = contentTypeOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public String getReferrerPolicy() {
|
||||
return referrerPolicy;
|
||||
public String getReferrerPolicyHeaderValue() {
|
||||
return referrerPolicyHeaderValue;
|
||||
}
|
||||
|
||||
public void setReferrerPolicy(String referrerPolicy) {
|
||||
this.referrerPolicy = referrerPolicy;
|
||||
public void setReferrerPolicyHeaderValue(String referrerPolicyHeaderValue) {
|
||||
this.referrerPolicyHeaderValue = referrerPolicyHeaderValue;
|
||||
}
|
||||
|
||||
public String getContentSecurityPolicy() {
|
||||
return contentSecurityPolicy;
|
||||
public String getContentSecurityPolicyHeaderValue() {
|
||||
return contentSecurityPolicyHeaderValue;
|
||||
}
|
||||
|
||||
public void setContentSecurityPolicy(String contentSecurityPolicy) {
|
||||
this.contentSecurityPolicy = contentSecurityPolicy;
|
||||
public void setContentSecurityPolicyHeaderValue(String contentSecurityPolicyHeaderValue) {
|
||||
this.contentSecurityPolicyHeaderValue = contentSecurityPolicyHeaderValue;
|
||||
}
|
||||
|
||||
public String getDownloadOptions() {
|
||||
return downloadOptions;
|
||||
public String getDownloadOptionsHeaderValue() {
|
||||
return downloadOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public void setDownloadOptions(String downloadOptions) {
|
||||
this.downloadOptions = downloadOptions;
|
||||
public void setDownloadOptionsHeaderValue(String downloadOptionHeaderValue) {
|
||||
this.downloadOptionsHeaderValue = downloadOptionsHeaderValue;
|
||||
}
|
||||
|
||||
public String getPermittedCrossDomainPolicies() {
|
||||
return permittedCrossDomainPolicies;
|
||||
public String getPermittedCrossDomainPoliciesHeaderValue() {
|
||||
return permittedCrossDomainPoliciesHeaderValue;
|
||||
}
|
||||
|
||||
public void setPermittedCrossDomainPolicies(String permittedCrossDomainPolicies) {
|
||||
this.permittedCrossDomainPolicies = permittedCrossDomainPolicies;
|
||||
public void setPermittedCrossDomainPoliciesHeaderValue(String permittedCrossDomainPoliciesHeaderValue) {
|
||||
this.permittedCrossDomainPoliciesHeaderValue = permittedCrossDomainPoliciesHeaderValue;
|
||||
}
|
||||
|
||||
public String getPermissionPolicyHeaderValue() {
|
||||
return permissionPolicyHeaderValue;
|
||||
}
|
||||
|
||||
public void setPermissionPolicyHeaderValue(String permissionPolicyHeaderValue) {
|
||||
this.permissionPolicyHeaderValue = permissionPolicyHeaderValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* bind the route specific/opt-in header names to enable, in lower case.
|
||||
*/
|
||||
void setEnable(Set<String> enable) {
|
||||
if (enable != null) {
|
||||
this.routeFilterConfigProvided = true;
|
||||
this.routeEnabledHeaders = enable.stream().map(String::toLowerCase).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the route specific/opt-in header names to enable, in lower case.
|
||||
*/
|
||||
Set<String> getRouteEnabledHeaders() {
|
||||
return routeEnabledHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* bind the route specific/opt-out header names to disable, in lower case.
|
||||
*/
|
||||
void setDisable(Set<String> disable) {
|
||||
if (disable != null) {
|
||||
this.routeFilterConfigProvided = true;
|
||||
this.routeDisabledHeaders = disable.stream().map(String::toLowerCase).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the route specific/opt-out header names to disable, in lower case
|
||||
*/
|
||||
Set<String> getRouteDisabledHeaders() {
|
||||
return routeDisabledHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the route specific/opt-out permission policies.
|
||||
*/
|
||||
String getRoutePermissionsPolicyHeaderValue() {
|
||||
return routePermissionsPolicyHeaderValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* bind the route specific/opt-out permissions policy.
|
||||
*/
|
||||
void setPermissionsPolicy(String permissionsPolicy) {
|
||||
this.routeFilterConfigProvided = true;
|
||||
this.routePermissionsPolicyHeaderValue = permissionsPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return flag whether route specific arguments were bound.
|
||||
*/
|
||||
boolean isRouteFilterConfigProvided() {
|
||||
return routeFilterConfigProvided;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,57 +16,135 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb, Thirunavukkarasu Ravichandran
|
||||
* @author Spencer Gibb, Thirunavukkarasu Ravichandran, Jörg Richter
|
||||
*/
|
||||
@ConfigurationProperties("spring.cloud.gateway.filter.secure-headers")
|
||||
public class SecureHeadersProperties {
|
||||
|
||||
/**
|
||||
* Xss-Protection header name.
|
||||
*/
|
||||
public static final String X_XSS_PROTECTION_HEADER = "X-Xss-Protection";
|
||||
|
||||
/**
|
||||
* Xss-Protection header default.
|
||||
*/
|
||||
public static final String X_XSS_PROTECTION_HEADER_DEFAULT = "1 ; mode=block";
|
||||
|
||||
/**
|
||||
* Strict transport security header name.
|
||||
*/
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER = "Strict-Transport-Security";
|
||||
|
||||
/**
|
||||
* Strict transport security header default.
|
||||
*/
|
||||
public static final String STRICT_TRANSPORT_SECURITY_HEADER_DEFAULT = "max-age=631138519";
|
||||
|
||||
/**
|
||||
* Frame options header name.
|
||||
*/
|
||||
public static final String X_FRAME_OPTIONS_HEADER = "X-Frame-Options";
|
||||
|
||||
/**
|
||||
* Frame Options header default.
|
||||
*/
|
||||
public static final String X_FRAME_OPTIONS_HEADER_DEFAULT = "DENY";
|
||||
|
||||
/**
|
||||
* Content-Type Options header name.
|
||||
*/
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
|
||||
|
||||
/**
|
||||
* Content-Type Options header default.
|
||||
*/
|
||||
public static final String X_CONTENT_TYPE_OPTIONS_HEADER_DEFAULT = "nosniff";
|
||||
|
||||
/**
|
||||
* Referrer Policy header name.
|
||||
*/
|
||||
public static final String REFERRER_POLICY_HEADER = "Referrer-Policy";
|
||||
|
||||
/**
|
||||
* Referrer Policy header default.
|
||||
*/
|
||||
public static final String REFERRER_POLICY_HEADER_DEFAULT = "no-referrer";
|
||||
|
||||
/**
|
||||
* Content-Security Policy header name.
|
||||
*/
|
||||
public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
|
||||
|
||||
/**
|
||||
* Content-Security Policy header default.
|
||||
*/
|
||||
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'";
|
||||
|
||||
/**
|
||||
* Download Options header name.
|
||||
*/
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER = "X-Download-Options";
|
||||
|
||||
/**
|
||||
* Download Options header default.
|
||||
*/
|
||||
public static final String X_DOWNLOAD_OPTIONS_HEADER_DEFAULT = "noopen";
|
||||
|
||||
/**
|
||||
* Permitted Cross-Domain Policies header name.
|
||||
*/
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER = "X-Permitted-Cross-Domain-Policies";
|
||||
|
||||
/**
|
||||
* Permitted Cross-Domain Policies header default.
|
||||
*/
|
||||
public static final String X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER_DEFAULT = "none";
|
||||
|
||||
/**
|
||||
* Permissions Policy header name. Opt-In required by external configuration.
|
||||
*/
|
||||
public static final String PERMISSIONS_POLICY_HEADER = "Permissions-Policy";
|
||||
|
||||
/**
|
||||
* Permissions Policy header default. Opt-In by external configuration required,
|
||||
* because the header default disables a comprehensive list of features.
|
||||
*/
|
||||
public static final String PERMISSIONS_POLICY_HEADER_OPT_IN_DEFAULT = "accelerometer=(), ambient-light-sensor=(), "
|
||||
+ "autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), "
|
||||
+ "encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), "
|
||||
+ "geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), "
|
||||
+ "navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), "
|
||||
+ "screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()";
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor for {@link SecureHeadersProperties}.
|
||||
* Initializes the `defaultHeaders` set with a predefined list of security headers.
|
||||
* The headers are transformed to lowercase for case-insensitive comparison.
|
||||
**/
|
||||
public SecureHeadersProperties() {
|
||||
|
||||
defaultHeaders = Stream.of(SecureHeadersProperties.X_XSS_PROTECTION_HEADER,
|
||||
SecureHeadersProperties.STRICT_TRANSPORT_SECURITY_HEADER,
|
||||
SecureHeadersProperties.X_FRAME_OPTIONS_HEADER, SecureHeadersProperties.X_CONTENT_TYPE_OPTIONS_HEADER,
|
||||
SecureHeadersProperties.REFERRER_POLICY_HEADER, SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER,
|
||||
SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER,
|
||||
SecureHeadersProperties.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
}
|
||||
|
||||
private String xssProtectionHeader = X_XSS_PROTECTION_HEADER_DEFAULT;
|
||||
|
||||
private String strictTransportSecurity = STRICT_TRANSPORT_SECURITY_HEADER_DEFAULT;
|
||||
@@ -83,7 +161,13 @@ public class SecureHeadersProperties {
|
||||
|
||||
private String permittedCrossDomainPolicies = X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER_DEFAULT;
|
||||
|
||||
private List<String> disable = new ArrayList<>();
|
||||
private String permissionsPolicy = PERMISSIONS_POLICY_HEADER_OPT_IN_DEFAULT;
|
||||
|
||||
private final Set<String> defaultHeaders;
|
||||
|
||||
private Set<String> enabledHeaders = new HashSet<>();
|
||||
|
||||
private Set<String> disabledHeaders = new HashSet<>();
|
||||
|
||||
public String getXssProtectionHeader() {
|
||||
return xssProtectionHeader;
|
||||
@@ -149,28 +233,73 @@ public class SecureHeadersProperties {
|
||||
this.permittedCrossDomainPolicies = permittedCrossDomainPolicies;
|
||||
}
|
||||
|
||||
public List<String> getDisable() {
|
||||
return disable;
|
||||
public String getPermissionsPolicy() {
|
||||
return permissionsPolicy;
|
||||
}
|
||||
|
||||
public void setPermissionsPolicy(String permissionsPolicy) {
|
||||
this.permissionsPolicy = permissionsPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default/opt-out header names to disable
|
||||
*/
|
||||
public List<String> getDisable() {
|
||||
return disabledHeaders.stream().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the list of default/opt-out header names to disable, transforms them into a lowercase set.
|
||||
* This is to ensure case-insensitive comparison.
|
||||
* @param disable - list of default/opt-out header names to disable
|
||||
*/
|
||||
public void setDisable(List<String> disable) {
|
||||
this.disable = disable;
|
||||
if (disable != null) {
|
||||
disabledHeaders = disable.stream().map(String::toLowerCase).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the opt-in header names to enable
|
||||
*/
|
||||
public Set<String> getEnabledHeaders() {
|
||||
return enabledHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the list of default/opt-out header names to enable, transforms them into a lowercase set.
|
||||
* This is to ensure case-insensitive comparison.
|
||||
* @param enable - list of default/opt-out header enable
|
||||
*/
|
||||
public void setEnable(List<String> enable) {
|
||||
if (enable != null) {
|
||||
enabledHeaders = enable.stream().map(String::toLowerCase).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default/opt-out header names to disable
|
||||
*/
|
||||
public Set<String> getDisabledHeaders() {
|
||||
return disabledHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default/opt-out header names to apply
|
||||
*/
|
||||
public Set<String> getDefaultHeaders() {
|
||||
return defaultHeaders;
|
||||
}
|
||||
|
||||
@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(", disabled='").append(disable).append('\'');
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
return "SecureHeadersProperties{" + "xssProtectionHeader='" + xssProtectionHeader + '\''
|
||||
+ ", strictTransportSecurity='" + strictTransportSecurity + '\'' + ", frameOptions='" + frameOptions
|
||||
+ '\'' + ", contentTypeOptions='" + contentTypeOptions + '\'' + ", referrerPolicy='" + referrerPolicy
|
||||
+ '\'' + ", contentSecurityPolicy='" + contentSecurityPolicy + '\'' + ", downloadOptions='"
|
||||
+ downloadOptions + '\'' + ", permittedCrossDomainPolicies='" + permittedCrossDomainPolicies + '\''
|
||||
+ ", permissionsPolicy='" + permissionsPolicy + '\'' + ", defaultHeaders=" + defaultHeaders
|
||||
+ ", enable=" + enabledHeaders + ", disable=" + disabledHeaders + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,19 +34,25 @@ import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
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.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.SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.PERMISSIONS_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.REFERRER_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.STRICT_TRANSPORT_SECURITY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_CONTENT_TYPE_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_FRAME_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER;
|
||||
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
|
||||
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
public class SecureHeadersGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
|
||||
/**
|
||||
* This test ensures that the response includes a set of default security headers,
|
||||
* which are defined in {@link SecureHeadersProperties}. It also confirms that the
|
||||
* opt-in "Permissions-Policy" header is not included in the response.
|
||||
*/
|
||||
@Test
|
||||
public void secureHeadersFilterWorks() {
|
||||
Mono<ClientResponse> result = webClient.get()
|
||||
@@ -59,7 +65,6 @@ public class SecureHeadersGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
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());
|
||||
@@ -70,6 +75,7 @@ public class SecureHeadersGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
assertThat(httpHeaders.getFirst(X_DOWNLOAD_OPTIONS_HEADER)).isEqualTo(defaults.getDownloadOptions());
|
||||
assertThat(httpHeaders.getFirst(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER))
|
||||
.isEqualTo(defaults.getPermittedCrossDomainPolicies());
|
||||
assertThat(httpHeaders.getOrEmpty(PERMISSIONS_POLICY_HEADER)).isEmpty();
|
||||
}).expectComplete().verify(DURATION);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -33,18 +37,19 @@ import static java.util.Arrays.asList;
|
||||
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;
|
||||
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;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.CONTENT_SECURITY_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.PERMISSIONS_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.REFERRER_POLICY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.STRICT_TRANSPORT_SECURITY_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_CONTENT_TYPE_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_DOWNLOAD_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_FRAME_OPTIONS_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER;
|
||||
import static org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties.X_XSS_PROTECTION_HEADER;
|
||||
|
||||
/**
|
||||
* @author Thirunavukkarasu Ravichandran
|
||||
* @author Thirunavukkarasu Ravichandran, Jörg Richter
|
||||
*/
|
||||
public class SecureHeadersGatewayFilterFactoryUnitTests {
|
||||
|
||||
@@ -75,7 +80,7 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders()).containsKeys(X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER,
|
||||
assertThat(response.getHeaders()).containsOnlyKeys(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);
|
||||
}
|
||||
@@ -105,8 +110,8 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(
|
||||
new SecureHeadersProperties());
|
||||
Config config = new Config();
|
||||
config.setStrictTransportSecurity("max-age=65535");
|
||||
config.setReferrerPolicy("referrer");
|
||||
config.setStrictTransportSecurityHeaderValue("max-age=65535");
|
||||
config.setReferrerPolicyHeaderValue("referrer");
|
||||
filter = filterFactory.apply(config);
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
@@ -135,13 +140,19 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
|
||||
@Test
|
||||
public void doesNotDuplicateHeaders() {
|
||||
String originalHeaderValue = "original-header-value";
|
||||
|
||||
SecureHeadersProperties secureHeadersProperties = new SecureHeadersProperties();
|
||||
secureHeadersProperties.setDisable(Collections.emptyList());
|
||||
secureHeadersProperties.setEnable(List.of(PERMISSIONS_POLICY_HEADER));
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(
|
||||
new SecureHeadersProperties());
|
||||
secureHeadersProperties);
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
String[] headers = { 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 };
|
||||
X_DOWNLOAD_OPTIONS_HEADER, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, PERMISSIONS_POLICY_HEADER };
|
||||
|
||||
for (String header : headers) {
|
||||
filter = filterFactory.apply(config);
|
||||
@@ -163,4 +174,91 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
|
||||
Assertions.assertThat(filter.toString()).contains("SecureHeaders");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doNotAddPermissionsPolicyWhenNotEnabled() {
|
||||
SecureHeadersProperties properties = new SecureHeadersProperties();
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
|
||||
filter = filterFactory.apply(new Config());
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
ServerHttpResponse response = captor.getValue().getResponse();
|
||||
assertThat(response.getHeaders()).doesNotContainKeys(PERMISSIONS_POLICY_HEADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPermissionsPolicyWhenEnabled() {
|
||||
SecureHeadersProperties properties = new SecureHeadersProperties();
|
||||
properties.setEnable(List.of("permissions-policy"));
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
|
||||
filter = filterFactory.apply(new Config());
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
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(PERMISSIONS_POLICY_HEADER))
|
||||
.containsExactly(SecureHeadersProperties.PERMISSIONS_POLICY_HEADER_OPT_IN_DEFAULT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPermissionsPolicyAndOverrideDefaults() {
|
||||
SecureHeadersProperties properties = new SecureHeadersProperties();
|
||||
properties.setEnable(List.of("permissions-policy"));
|
||||
properties.setPermissionsPolicy("camera=*");
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
|
||||
filter = filterFactory.apply(new Config());
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
ServerHttpResponse response = captor.getValue().getResponse();
|
||||
assertThat(response.getHeaders().get(PERMISSIONS_POLICY_HEADER)).containsExactly("camera=*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyCompositionWithDisabledHeadersAndPermissionPolicy() {
|
||||
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"));
|
||||
properties.setEnable(List.of("permissions-policy"));
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
|
||||
filter = filterFactory.apply(new Config());
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
ServerHttpResponse response = captor.getValue().getResponse();
|
||||
assertThat(response.getHeaders()).containsOnlyKeys(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER,
|
||||
PERMISSIONS_POLICY_HEADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overrideDefaultInSecurityPropertiesWhenRouteConfigIsProvided() {
|
||||
|
||||
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(new SecureHeadersProperties());
|
||||
|
||||
Config config = new Config();
|
||||
config.setDisable(Set.of("strict-transport-security"));
|
||||
config.setEnable(Set.of("permissions-policy"));
|
||||
config.setPermissionsPolicy("camera=*");
|
||||
|
||||
filter = filterFactory.apply(config);
|
||||
|
||||
filter.filter(exchange, filterChain).block();
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders()).containsOnlyKeys(X_XSS_PROTECTION_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,
|
||||
PERMISSIONS_POLICY_HEADER);
|
||||
assertThat(response.getHeaders().get(PERMISSIONS_POLICY_HEADER)).containsExactly("camera=*");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user