diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index d3070699f5..1a76c76ecc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -34,6 +34,7 @@ import org.springframework.security.web.header.writers.ContentSecurityPolicyHead import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; +import org.springframework.security.web.header.writers.PermissionsPolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter; @@ -93,6 +94,8 @@ public class HeadersConfigurer> private final FeaturePolicyConfig featurePolicy = new FeaturePolicyConfig(); + private final PermissionsPolicyConfig permissionsPolicy = new PermissionsPolicyConfig(); + /** * Creates a new instance * @@ -387,6 +390,7 @@ public class HeadersConfigurer> addIfNotNull(writers, this.contentSecurityPolicy.writer); addIfNotNull(writers, this.referrerPolicy.writer); addIfNotNull(writers, this.featurePolicy.writer); + addIfNotNull(writers, this.permissionsPolicy.writer); writers.addAll(this.headerWriters); return writers; } @@ -487,12 +491,58 @@ public class HeadersConfigurer> * @throws IllegalArgumentException if policyDirectives is {@code null} or empty * @since 5.1 * @see FeaturePolicyHeaderWriter + * @deprecated Use {@link #permissionsPolicy(Customizer)} instead. */ + @Deprecated public FeaturePolicyConfig featurePolicy(String policyDirectives) { this.featurePolicy.writer = new FeaturePolicyHeaderWriter(policyDirectives); return this.featurePolicy; } + /** + *

+ * Allows configuration for + * Permissions + * Policy. + *

+ * + *

+ * Configuration is provided to the {@link PermissionsPolicyHeaderWriter} which + * support the writing of the header as detailed in the W3C Technical Report: + *

+ *
    + *
  • Permissions-Policy
  • + *
+ * @return the {@link PermissionsPolicyConfig} for additional configuration + * @since 5.5 + * @see PermissionsPolicyHeaderWriter + */ + public PermissionsPolicyConfig permissionsPolicy() { + this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter(); + return this.permissionsPolicy; + } + + /** + * Allows configuration for + * Permissions + * Policy. + *

+ * Calling this method automatically enables (includes) the {@code Permissions-Policy} + * header in the response using the supplied policy directive(s). + *

+ * Configuration is provided to the {@link PermissionsPolicyHeaderWriter} which is + * responsible for writing the header. + * @return the {@link PermissionsPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + * @since 5.5 + * @see PermissionsPolicyHeaderWriter + */ + public PermissionsPolicyConfig permissionsPolicy(Customizer permissionsPolicyCustomizer) { + this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter(); + permissionsPolicyCustomizer.customize(this.permissionsPolicy); + return this.permissionsPolicy; + } + public final class ContentTypeOptionsConfig { private XContentTypeOptionsHeaderWriter writer; @@ -1063,4 +1113,33 @@ public class HeadersConfigurer> } + public final class PermissionsPolicyConfig { + + private PermissionsPolicyHeaderWriter writer; + + private PermissionsPolicyConfig() { + } + + /** + * Sets the policy to be used in the response header. + * @param policy a permissions policy + * @return the {@link PermissionsPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policy is null + */ + public PermissionsPolicyConfig policy(String policy) { + this.writer.setPolicy(policy); + return this; + } + + /** + * Allows completing configuration of Permissions Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index ac37bc76ab..7f42ff724e 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -39,6 +39,7 @@ import org.springframework.security.web.header.writers.ContentSecurityPolicyHead import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; +import org.springframework.security.web.header.writers.PermissionsPolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; import org.springframework.security.web.header.writers.StaticHeadersWriter; @@ -119,6 +120,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String FEATURE_POLICY_ELEMENT = "feature-policy"; + private static final String PERMISSIONS_POLICY_ELEMENT = "permissions-policy"; + private static final String ALLOW_FROM = "ALLOW-FROM"; private ManagedList headerWriters; @@ -140,6 +143,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { parseContentSecurityPolicyElement(disabled, element, parserContext); parseReferrerPolicyElement(element, parserContext); parseFeaturePolicyElement(element, parserContext); + parsePermissionsPolicyElement(element, parserContext); parseHeaderElements(element); boolean noWriters = this.headerWriters.isEmpty(); if (disabled && !noWriters) { @@ -351,6 +355,27 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { this.headerWriters.add(headersWriter.getBeanDefinition()); } + private void parsePermissionsPolicyElement(Element element, ParserContext context) { + Element permissionsPolicyElement = (element != null) + ? DomUtils.getChildElementByTagName(element, PERMISSIONS_POLICY_ELEMENT) : null; + if (permissionsPolicyElement != null) { + addPermissionsPolicy(permissionsPolicyElement, context); + } + } + + private void addPermissionsPolicy(Element permissionsPolicyElement, ParserContext context) { + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder + .genericBeanDefinition(PermissionsPolicyHeaderWriter.class); + String policyDirectives = permissionsPolicyElement.getAttribute(ATT_POLICY); + if (!StringUtils.hasText(policyDirectives)) { + context.getReaderContext().error(ATT_POLICY + " requires a 'value' to be set.", permissionsPolicyElement); + } + else { + headersWriter.addConstructorArgValue(policyDirectives); + } + this.headerWriters.add(headersWriter.getBeanDefinition()); + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error("Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.", element); diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 2cbbaeb231..89e650530b 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -149,6 +149,7 @@ import org.springframework.security.web.server.header.ContentSecurityPolicyServe import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter; +import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy; import org.springframework.security.web.server.header.ServerHttpHeadersWriter; @@ -2232,13 +2233,16 @@ public class ServerHttpSecurity { private FeaturePolicyServerHttpHeadersWriter featurePolicy = new FeaturePolicyServerHttpHeadersWriter(); + private PermissionsPolicyServerHttpHeadersWriter permissionsPolicy = new PermissionsPolicyServerHttpHeadersWriter(); + private ContentSecurityPolicyServerHttpHeadersWriter contentSecurityPolicy = new ContentSecurityPolicyServerHttpHeadersWriter(); private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter(); private HeaderSpec() { this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts, - this.frameOptions, this.xss, this.featurePolicy, this.contentSecurityPolicy, this.referrerPolicy)); + this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy, + this.referrerPolicy)); } /** @@ -2395,13 +2399,32 @@ public class ServerHttpSecurity { /** * Configures {@code Feature-Policy} response header. - * @param policyDirectives the policy directive(s) + * @param policyDirectives the policy * @return the {@link FeaturePolicySpec} to configure */ public FeaturePolicySpec featurePolicy(String policyDirectives) { return new FeaturePolicySpec(policyDirectives); } + /** + * Configures {@code Permissions-Policy} response header. + * @return the {@link PermissionsPolicySpec} to configure + */ + public PermissionsPolicySpec permissionsPolicy() { + return new PermissionsPolicySpec(); + } + + /** + * Configures {@code Permissions-Policy} response header. + * @param permissionsPolicyCustomizer the {@link Customizer} to provide more + * options for the {@link PermissionsPolicySpec} + * @return the {@link HeaderSpec} to customize + */ + public HeaderSpec permissionsPolicy(Customizer permissionsPolicyCustomizer) { + permissionsPolicyCustomizer.customize(new PermissionsPolicySpec()); + return this; + } + /** * Configures {@code Referrer-Policy} response header. * @param referrerPolicy the policy to use @@ -2677,6 +2700,38 @@ public class ServerHttpSecurity { } + /** + * Configures {@code Permissions-Policy} response header. + * + * @since 5.5 + * @see #permissionsPolicy() + */ + public final class PermissionsPolicySpec { + + private PermissionsPolicySpec() { + } + + /** + * Sets the policy to be used in the response header. + * @param policy a permissions policy + * @return the {@link PermissionsPolicySpec} to continue configuring + */ + public PermissionsPolicySpec policy(String policy) { + HeaderSpec.this.permissionsPolicy.setPolicy(policy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + /** * Configures {@code Referrer-Policy} response header. * diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt index b6c435c2ff..06e9eeeaaa 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -34,6 +34,7 @@ class ServerHeadersDsl { private var contentSecurityPolicy: ((ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit)? = null private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null private var featurePolicyDirectives: String? = null + private var permissionsPolicy: ((ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit)? = null private var disabled = false @@ -140,6 +141,21 @@ class ServerHeadersDsl { this.featurePolicyDirectives = policyDirectives } + /** + * Allows configuration for Permissions + * Policy. + * + *

+ * Calling this method automatically enables (includes) the Permissions-Policy + * header in the response using the supplied policy directive(s). + *

+ * + * @param permissionsPolicyConfig the customization to apply to the header + */ + fun permissionsPolicy(permissionsPolicyConfig: ServerPermissionsPolicyDsl.() -> Unit) { + this.permissionsPolicy = ServerPermissionsPolicyDsl().apply(permissionsPolicyConfig).get() + } + /** * Disables HTTP response headers. */ @@ -170,6 +186,9 @@ class ServerHeadersDsl { featurePolicyDirectives?.also { headers.featurePolicy(featurePolicyDirectives) } + permissionsPolicy?.also { + headers.permissionsPolicy(permissionsPolicy) + } referrerPolicy?.also { headers.referrerPolicy(referrerPolicy) } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDsl.kt new file mode 100644 index 0000000000..912cd51b9d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDsl.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] permissions policy header using + * idiomatic Kotlin code. + * + * @author Christophe Gilles + * @since 5.5 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerPermissionsPolicyDsl { + var policy: String? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit { + return { permissionsPolicy -> + policy?.also { + permissionsPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt index 833f2474b7..36b42f2371 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt @@ -41,6 +41,7 @@ class HeadersDsl { private var contentSecurityPolicy: ((HeadersConfigurer.ContentSecurityPolicyConfig) -> Unit)? = null private var referrerPolicy: ((HeadersConfigurer.ReferrerPolicyConfig) -> Unit)? = null private var featurePolicyDirectives: String? = null + private var permissionsPolicy: ((HeadersConfigurer.PermissionsPolicyConfig) -> Unit)? = null private var disabled = false private var headerWriters = mutableListOf() @@ -164,6 +165,21 @@ class HeadersDsl { this.featurePolicyDirectives = policyDirectives } + /** + * Allows configuration for Permissions + * Policy. + * + *

+ * Calling this method automatically enables (includes) the Permissions-Policy + * header in the response using the supplied policy directive(s). + *

+ * + * @param policyDirectives policyDirectives the security policy directive(s) + */ + fun permissionsPolicy(permissionsPolicyConfig: PermissionsPolicyDsl.() -> Unit) { + this.permissionsPolicy = PermissionsPolicyDsl().apply(permissionsPolicyConfig).get() + } + /** * Adds a [HeaderWriter] instance. * @@ -217,6 +233,9 @@ class HeadersDsl { featurePolicyDirectives?.also { headers.featurePolicy(featurePolicyDirectives) } + permissionsPolicy?.also { + headers.permissionsPolicy(permissionsPolicy) + } headerWriters.forEach { headerWriter -> headers.addHeaderWriter(headerWriter) } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/PermissionsPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/PermissionsPolicyDsl.kt new file mode 100644 index 0000000000..e668931bcd --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/PermissionsPolicyDsl.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer + +/** + * A Kotlin DSL to configure the [HttpSecurity] permissions policy header using + * idiomatic Kotlin code. + * + * @author Christophe Gilles + * @since 5.5 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class PermissionsPolicyDsl { + var policy: String? = null + + internal fun get(): (HeadersConfigurer.PermissionsPolicyConfig) -> Unit { + return { permissionsPolicy -> + policy?.also { + permissionsPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc index 21444e8059..8fab2d648f 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc @@ -919,7 +919,7 @@ csrf-options.attlist &= headers = ## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. -element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & header*)} +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & header*)} headers-options.attlist &= ## Specifies if the default headers should be disabled. Default false. attribute defaults-disabled {xsd:token}? @@ -1007,6 +1007,13 @@ feature-options.attlist &= ## The security policy directive(s) for the Feature-Policy header. attribute policy-directives {xsd:token}? +permissions-policy = + ## Adds support for Permissions Policy + element permissions-policy {permissions-options.attlist} +permissions-options.attlist &= + ## The policies for the Permissions-Policy header. + attribute policy {xsd:token}? + cache-control = ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request element cache-control {cache-control.attlist} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd index 3b18dcc64c..4c1e7f9182 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd @@ -2692,6 +2692,7 @@ + @@ -2926,6 +2927,23 @@ + + + Adds support for Permissions Policy + + + + + + + + + + The policies for the Permissions-Policy header. + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java index 497fe083aa..dffe4390e5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java @@ -447,6 +447,44 @@ public class HeadersConfigurerTests { .withRootCauseInstanceOf(IllegalArgumentException.class); } + @Test + public void getWhenPermissionsPolicyConfiguredThenPermissionsPolicyHeaderInResponse() throws Exception { + this.spring.register(PermissionsPolicyConfig.class).autowire(); + ResultMatcher permissionsPolicy = header().string("Permissions-Policy", "geolocation=(self)"); + // @formatter:off + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(permissionsPolicy) + .andReturn(); + // @formatter:on + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Permissions-Policy"); + } + + @Test + public void getWhenPermissionsPolicyConfiguredWithStringThenPermissionsPolicyHeaderInResponse() throws Exception { + this.spring.register(PermissionsPolicyStringConfig.class).autowire(); + ResultMatcher permissionsPolicy = header().string("Permissions-Policy", "geolocation=(self)"); + // @formatter:off + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(permissionsPolicy) + .andReturn(); + // @formatter:on + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Permissions-Policy"); + } + + @Test + public void configureWhenPermissionsPolicyEmptyThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(PermissionsPolicyInvalidConfig.class).autowire()) + .withRootCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void configureWhenPermissionsPolicyStringEmptyThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(PermissionsPolicyInvalidStringConfig.class).autowire()) + .withRootCauseInstanceOf(IllegalArgumentException.class); + } + @Test public void getWhenHstsConfiguredWithPreloadThenStrictTransportSecurityHeaderWithPreloadInResponse() throws Exception { @@ -1012,6 +1050,68 @@ public class HeadersConfigurerTests { } + @EnableWebSecurity + static class PermissionsPolicyConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers() + .defaultsDisabled() + .permissionsPolicy((permissionsPolicy) -> permissionsPolicy.policy("geolocation=(self)")); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermissionsPolicyStringConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers() + .defaultsDisabled() + .permissionsPolicy() + .policy("geolocation=(self)"); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermissionsPolicyInvalidConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers() + .defaultsDisabled() + .permissionsPolicy((permissionsPolicy) -> permissionsPolicy.policy(null)); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermissionsPolicyInvalidStringConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers() + .defaultsDisabled() + .permissionsPolicy() + .policy(""); + // @formatter:on + } + + } + @EnableWebSecurity static class HstsWithPreloadConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java index 0d094ed267..59dda6b7b0 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -332,6 +332,17 @@ public class HttpHeadersConfigTests { () -> this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderValue")).autowire()); } + @Test + public void requestWhenPermissionsPolicyConfiguredWithGeolocationSelfThenGeolocationSelf() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithPermissionsPolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Permissions-Policy", "geolocation=(self)")); + // @formatter:on + } + @Test public void requestWhenUsingXssProtectionThenDefaultsToModeBlock() throws Exception { Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDslTests.kt new file mode 100644 index 0000000000..9f161837a6 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerPermissionsPolicyDslTests.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerPermissionsPolicyDsl] + * + * @author Christophe Gilles + */ +class ServerPermissionsPolicyDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when permissions policy configured then permissions policy header in response`() { + this.spring.register(PermissionsPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist("Permissions-Policy") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PermissionsPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + permissionsPolicy { } + } + } + } + } + + @Test + fun `request when custom policy configured then custom policy in response header`() { + this.spring.register(CustomPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Permissions-Policy", "geolocation=(self)") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + permissionsPolicy { + policy = "geolocation=(self)" + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt index c16c504710..f4ce0a5d03 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt @@ -24,6 +24,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.servlet.headers.PermissionsPolicyDsl import org.springframework.security.web.header.writers.StaticHeadersWriter import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter @@ -93,6 +94,29 @@ class HeadersDslTests { } } + @Test + fun `headers when permissions policy configured then header in response`() { + this.spring.register(PermissionsPolicyConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { string("Permissions-Policy", "geolocation=(self)") } + } + } + + @EnableWebSecurity + open class PermissionsPolicyConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + permissionsPolicy { + policy = "geolocation=(self)" + } + } + } + } + } + @Test fun `request when headers disabled then no security headers are in the response`() { this.spring.register(HeadersDisabledConfig::class.java).autowire() diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithPermissionsPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithPermissionsPolicy.xml new file mode 100644 index 0000000000..f753f6a20a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithPermissionsPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc index 05ee7f5943..ee3639d937 100644 --- a/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc @@ -339,6 +339,28 @@ With Feature Policy, developers can opt-in to a set of "policies" for the browse These policies restrict what APIs the site can access or modify the browser's default behavior for certain features. +[[headers-permissions]] +== Permissions Policy + +[NOTE] +==== +Refer to the relevant sections to see how to configure both <> and <> based applications. +==== + +https://w3c.github.io/webappsec-permissions-policy/[Permissions Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. + +.Permissions Policy Example +==== +[source] +---- +Permissions-Policy: geolocation=(self) +---- +==== + +With Permissions Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site. +These policies restrict what APIs the site can access or modify the browser's default behavior for certain features. + + [[headers-clear-site-data]] == Clear Site Data diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/exploits/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/exploits/headers.adoc index 9b1f2041f6..2ee82ad2a6 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/exploits/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/exploits/headers.adoc @@ -472,6 +472,58 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { ==== +[[webflux-headers-permissions]] +== Permissions Policy + +Spring Security does not add <> headers by default. +The following `Permissions-Policy` header: + +.Permissions-Policy Example +==== +[source] +---- +Permissions-Policy: geolocation=(self) +---- +==== + +You can enable the Permissions Policy header as shown below: + +.Permissions-Policy Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .permissionsPolicy(permissions -> permissions + .policy("geolocation=(self)") + ) + ); + return http.build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + permissionsPolicy { + policy = "geolocation=(self)" + } + } + } +} +---- +==== + + [[webflux-headers-clear-site-data]] == Clear Site Data diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/headers.adoc index ecb34b9c5d..e45c5c96c9 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/headers.adoc @@ -816,6 +816,76 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { ---- ==== +[[servlet-headers-permissions]] +== Permissions Policy + +Spring Security does not add <> headers by default. +The following `Permissions-Policy` header: + +.Permissions-Policy Example +==== +[source] +---- +Permissions-Policy: geolocation=(self) +---- +==== + +can enable the Permissions Policy header using the configuration shown below: + +.Permissions-Policy +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class WebSecurityConfig extends +WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // ... + .headers(headers -> headers + .permissionsPolicy(permissions -> permissions + .policy("geolocation=(self)") + ) + ); + } +} +---- + +.XML +[source,xml,role="secondary"] +---- + + + + + + + +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class SecurityConfig : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + // ... + headers { + permissionPolicy { + policy = "geolocation=(self)" + } + } + } + } +} +---- +==== + [[servlet-headers-clear-site-data]] == Clear Site Data diff --git a/web/src/main/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriter.java new file mode 100644 index 0000000000..aaefc3d9ca --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Provides support for + * Permisisons Policy. + *

+ * Permissions Policy allows web developers to selectively enable, disable, and modify the + * behavior of certain APIs and web features in the browser. + *

+ * A declaration of a permissions policy contains a set of security policies, each + * responsible for declaring the restrictions for a particular feature type. + * + * @author Christophe Gilles + * @since 5.5 + */ +public final class PermissionsPolicyHeaderWriter implements HeaderWriter { + + private static final String PERMISSIONS_POLICY_HEADER = "Permissions-Policy"; + + private String policy; + + /** + * Create a new instance of {@link PermissionsPolicyHeaderWriter}. + */ + public PermissionsPolicyHeaderWriter() { + } + + /** + * Create a new instance of {@link PermissionsPolicyHeaderWriter} with supplied + * security policy. + * @param policy the security policy + * @throws IllegalArgumentException if policy is {@code null} or empty + */ + public PermissionsPolicyHeaderWriter(String policy) { + setPolicy(policy); + } + + /** + * Sets the policy to be used in the response header. + * @param policy a permissions policy + * @throws IllegalArgumentException if policy is null + */ + public void setPolicy(String policy) { + Assert.hasLength(policy, "policy can not be null or empty"); + this.policy = policy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (!response.containsHeader(PERMISSIONS_POLICY_HEADER)) { + response.setHeader(PERMISSIONS_POLICY_HEADER, this.policy); + } + } + + @Override + public String toString() { + return getClass().getName() + " [policy=" + this.policy + "]"; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriter.java new file mode 100644 index 0000000000..15ea678e81 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.security.web.server.header.StaticServerHttpHeadersWriter.Builder; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Writes the {@code Permissions-Policy} response header with configured policy + * directives. + * + * @author Christophe Gilles + * @since 5.5 + */ +public final class PermissionsPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String PERMISSIONS_POLICY = "Permissions-Policy"; + + private ServerHttpHeadersWriter delegate; + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(String policyDirectives) { + Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(PERMISSIONS_POLICY, policyDirectives); + return builder.build(); + } + + /** + * Set the policy to be used in the response header. + * @param policy the policy + * @throws IllegalArgumentException if policy is {@code null} + */ + public void setPolicy(String policy) { + Assert.notNull(policy, "policy must not be null"); + this.delegate = createDelegate(policy); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriterTests.java new file mode 100644 index 0000000000..54a8a00437 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/PermissionsPolicyHeaderWriterTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PermissionsPolicyHeaderWriter}. + * + * @author Christophe Gilles + */ +public class PermissionsPolicyHeaderWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation=(self)"; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private PermissionsPolicyHeaderWriter writer; + + private static final String PERMISSIONS_POLICY_HEADER = "Permissions-Policy"; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.writer = new PermissionsPolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersPermissionsPolicyDefault() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader("Permissions-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void createWriterWithNullPolicyShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new PermissionsPolicyHeaderWriter(null)) + .withMessage("policy can not be null or empty"); + } + + @Test + public void createWriterWithEmptyPolicyShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new PermissionsPolicyHeaderWriter("")) + .withMessage("policy can not be null or empty"); + } + + @Test + public void writeHeaderOnlyIfNotPresent() { + String value = new String("value"); + this.response.setHeader(PERMISSIONS_POLICY_HEADER, value); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(PERMISSIONS_POLICY_HEADER)).isSameAs(value); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..7f28500eeb --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/PermissionsPolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PermissionsPolicyServerHttpHeadersWriter}. + * + * @author Christophe Gilles + */ +public class PermissionsPolicyServerHttpHeadersWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation=(self)"; + + private ServerWebExchange exchange; + + private PermissionsPolicyServerHttpHeadersWriter writer; + + @Before + public void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new PermissionsPolicyServerHttpHeadersWriter(); + } + + @Test + public void writeHeadersWhenUsingDefaultsThenDoesNotWrite() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + public void writeHeadersWhenUsingPolicyThenWritesPolicy() { + this.writer.setPolicy(DEFAULT_POLICY_DIRECTIVES); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY)) + .containsOnly(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersWhenAlreadyWrittenThenWritesHeader() { + this.writer.setPolicy(DEFAULT_POLICY_DIRECTIVES); + String headerValue = "camera=(self)"; + this.exchange.getResponse().getHeaders().set(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY, + headerValue); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(PermissionsPolicyServerHttpHeadersWriter.PERMISSIONS_POLICY)).containsOnly(headerValue); + } + +}