diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc
index 84254642..12177b8a 100644
--- a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc
+++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/secureheaders-factory.adoc
@@ -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.
diff --git a/spring-cloud-gateway-sample/src/main/resources/application-secureheaders.yml b/spring-cloud-gateway-sample/src/main/resources/application-secureheaders.yml
new file mode 100644
index 00000000..ddae382f
--- /dev/null
+++ b/spring-cloud-gateway-sample/src/main/resources/application-secureheaders.yml
@@ -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: '*'
+
+
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 3a079968..ce699763 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
@@ -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 Http-Security-Headers.
+ * 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 {
@@ -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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- HttpHeaders headers = exchange.getResponse().getHeaders();
- List disabled = properties.getDisable();
+ HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
+ Set 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 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 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 assembleHeaders(Config config, SecureHeadersProperties properties) {
+ Set 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 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 routeEnabledHeaders = new HashSet<>();
- private String strictTransportSecurity;
+ private Set 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 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 getRouteEnabledHeaders() {
+ return routeEnabledHeaders;
+ }
+
+ /**
+ * bind the route specific/opt-out header names to disable, in lower case.
+ */
+ void setDisable(Set 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 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;
}
}
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java
index 6802575a..f9ec7b35 100644
--- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersProperties.java
@@ -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 disable = new ArrayList<>();
+ private String permissionsPolicy = PERMISSIONS_POLICY_HEADER_OPT_IN_DEFAULT;
+
+ private final Set defaultHeaders;
+
+ private Set enabledHeaders = new HashSet<>();
+
+ private Set disabledHeaders = new HashSet<>();
public String getXssProtectionHeader() {
return xssProtectionHeader;
@@ -149,28 +233,73 @@ public class SecureHeadersProperties {
this.permittedCrossDomainPolicies = permittedCrossDomainPolicies;
}
- public List 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 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 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 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 enable) {
+ if (enable != null) {
+ enabledHeaders = enable.stream().map(String::toLowerCase).collect(Collectors.toUnmodifiableSet());
+ }
+ }
+
+ /**
+ * @return the default/opt-out header names to disable
+ */
+ public Set getDisabledHeaders() {
+ return disabledHeaders;
+ }
+
+ /**
+ * @return the default/opt-out header names to apply
+ */
+ public Set 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 + '}';
}
}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryTests.java
index 1742a19e..75751f05 100644
--- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryTests.java
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SecureHeadersGatewayFilterFactoryTests.java
@@ -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 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);
}
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 65dc9809..46284f91 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
@@ -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=*");
+ }
+
}