From c6ea447cc09ff21843197bc1baf936677371c9f9 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Wed, 15 Aug 2018 22:05:10 +0200 Subject: [PATCH] Add support for Feature-Policy security header --- .../web/configurers/HeadersConfigurer.java | 47 +++++++++++- .../http/HeadersBeanDefinitionParser.java | 33 +++++++- .../security/config/spring-security-5.1.rnc | 9 ++- .../security/config/spring-security-5.1.xsd | 18 ++++- .../configurers/HeadersConfigurerTests.groovy | 47 +++++++++++- .../_includes/appendix/namespace.adoc | 20 +++++ .../docs/asciidoc/_includes/web/headers.adoc | 50 ++++++++++++ .../writers/FeaturePolicyHeaderWriter.java | 76 +++++++++++++++++++ .../FeaturePolicyHeaderWriterTests.java | 73 ++++++++++++++++++ 9 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java create mode 100644 web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java 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 992529ab57..ba8652840c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.annotation.web.configurers; import java.net.URI; @@ -58,6 +59,7 @@ import org.springframework.util.Assert; * @author Tim Ysewyn * @author Joe Grandja * @author Eddú Meléndez + * @author Vedran Pavic * @since 3.2 */ public class HeadersConfigurer> extends @@ -82,6 +84,8 @@ public class HeadersConfigurer> extends private final ReferrerPolicyConfig referrerPolicy = new ReferrerPolicyConfig(); + private final FeaturePolicyConfig featurePolicy = new FeaturePolicyConfig(); + /** * Creates a new instance * @@ -775,6 +779,7 @@ public class HeadersConfigurer> extends addIfNotNull(writers, hpkp.writer); addIfNotNull(writers, contentSecurityPolicy.writer); addIfNotNull(writers, referrerPolicy.writer); + addIfNotNull(writers, featurePolicy.writer); writers.addAll(headerWriters); return writers; } @@ -848,4 +853,44 @@ public class HeadersConfigurer> extends } } + + /** + * Allows configuration for Feature + * Policy. + *

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

+ * Configuration is provided to the {@link FeaturePolicyHeaderWriter} which is + * responsible for writing the header. + * + * @see FeaturePolicyHeaderWriter + * @since 5.1 + * @return the {@link FeaturePolicyHeaderWriter} for additional configuration + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyConfig featurePolicy(String policyDirectives) { + this.featurePolicy.writer = new FeaturePolicyHeaderWriter(policyDirectives); + return featurePolicy; + } + + public final class FeaturePolicyConfig { + + private FeaturePolicyHeaderWriter writer; + + private FeaturePolicyConfig() { + } + + /** + * Allows completing configuration of Feature 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 20ac10dab7..dffeab94a0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.http; import java.net.URI; @@ -47,6 +48,7 @@ import org.w3c.dom.Node; * @author Marten Deinum * @author Tim Ysewyn * @author Eddú Meléndez + * @author Vedran Pavic * @since 3.2 */ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { @@ -85,6 +87,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy"; private static final String REFERRER_POLICY_ELEMENT = "referrer-policy"; + private static final String FEATURE_POLICY_ELEMENT = "feature-policy"; private static final String ALLOW_FROM = "ALLOW-FROM"; @@ -114,6 +117,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { parseReferrerPolicyElement(element, parserContext); + parseFeaturePolicyElement(element, parserContext); + parseHeaderElements(element); boolean noWriters = headerWriters.isEmpty(); @@ -313,6 +318,32 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { headerWriters.add(headersWriter.getBeanDefinition()); } + private void parseFeaturePolicyElement(Element element, ParserContext context) { + Element featurePolicyElement = (element == null) ? null + : DomUtils.getChildElementByTagName(element, FEATURE_POLICY_ELEMENT); + if (featurePolicyElement != null) { + addFeaturePolicy(featurePolicyElement, context); + } + } + + private void addFeaturePolicy(Element featurePolicyElement, ParserContext context) { + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder + .genericBeanDefinition(FeaturePolicyHeaderWriter.class); + + String policyDirectives = featurePolicyElement + .getAttribute(ATT_POLICY_DIRECTIVES); + if (!StringUtils.hasText(policyDirectives)) { + context.getReaderContext().error( + ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", + featurePolicyElement); + } + else { + headersWriter.addConstructorArgValue(policyDirectives); + } + + headerWriters.add(headersWriter.getBeanDefinition()); + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error( diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc index f3b75156ad..af761497c9 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc @@ -743,7 +743,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? & header*)} +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & header*)} headers-options.attlist &= ## Specifies if the default headers should be disabled. Default false. attribute defaults-disabled {xsd:boolean}? @@ -821,6 +821,13 @@ referrer-options.attlist &= ## The policies for the Referrer-Policy header. attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {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.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd index 773d9e55af..e6c0f5a52a 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -2253,6 +2253,7 @@ + @@ -2464,6 +2465,21 @@ + + + Adds support for Feature Policy + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for @@ -2719,4 +2735,4 @@ - \ No newline at end of file + diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy index 07fa282114..07feb338f1 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.annotation.web.configurers import org.springframework.beans.factory.BeanCreationException @@ -20,14 +21,17 @@ import org.springframework.security.config.annotation.BaseSpringSpec 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 static org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy /** + * Tests for {@link HeadersConfigurer}. * * @author Rob Winch * @author Tim Ysewyn * @author Joe Grandja * @author Eddú Meléndez + * @author Vedran Pavic */ class HeadersConfigurerTests extends BaseSpringSpec { @@ -497,4 +501,45 @@ class HeadersConfigurerTests extends BaseSpringSpec { } } + def "headers.featurePolicy default header"() { + setup: + loadConfig(FeaturePolicyDefaultConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request, response, chain) + then: + responseHeaders == ['Feature-Policy': 'geolocation \'self\''] + } + + @EnableWebSecurity + static class FeaturePolicyDefaultConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy("geolocation 'self'"); + } + } + + def "headers.featurePolicy empty policyDirectives"() { + when: + loadConfig(FeaturePolicyInvalidConfig) + then: + thrown(BeanCreationException) + } + + @EnableWebSecurity + static class FeaturePolicyInvalidConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy(""); + } + } + } diff --git a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc index cc6c3a2bb8..462330aa8f 100644 --- a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc @@ -241,6 +241,7 @@ This allows HTTPS websites to resist impersonation by attackers using mis-issued ** `Content-Security-Policy` or `Content-Security-Policy-Report-Only` - Can be set using the <> element. https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). ** `Referrer-Policy` - Can be set using the <> element, https://www.w3.org/TR/referrer-policy/[Referrer-Policy] is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on. +** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-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. [[nsa-headers-attributes]] ===== Attributes @@ -272,6 +273,7 @@ The default is false (the headers are enabled). * <> * <> * <> +* <> * <> * <> * <> @@ -459,6 +461,24 @@ Default "no-referrer". +[[nsa-feature-policy]] +==== +When enabled adds the https://wicg.github.io/feature-policy/[Feature Policy] header to the response. + +[[nsa-feature-policy-attributes]] +===== Attributes + +[[nsa-feature-policy-policy-directives]] +* **policy-directives** +The security policy directive(s) for the Feature-Policy header. + +[[nsa-feature-policy-parents]] +===== Parent Elements of + +* <> + + + [[nsa-frame-options]] ==== When enabled adds the http://tools.ietf.org/html/draft-ietf-websec-x-frame-options[X-Frame-Options header] to the response, this allows newer browsers to do some security checks and prevent http://en.wikipedia.org/wiki/Clickjacking[clickjacking] attacks. diff --git a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc index fd5bb14668..3873dc2df7 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc @@ -714,6 +714,56 @@ protected void configure(HttpSecurity http) throws Exception { ---- +[[headers-feature]] +==== Feature Policy + +https://wicg.github.io/feature-policy/[Feature 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. + +[source] +---- +Feature-Policy: geolocation 'self' +---- + +With Feature 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-feature-configure]] +===== Configuring Feature Policy + +Spring Security *_doesn't add_* Feature Policy header by default. + +You can enable the Feature-Policy header using XML configuration with the <>> element as shown below: + +[source,xml] +---- + + + + + + + +---- + +Similarly, you can enable the Feature Policy header using Java configuration as shown below: + +[source,java] +---- +@EnableWebSecurity +public class WebSecurityConfig extends +WebSecurityConfigurerAdapter { + +@Override +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .headers() + .featurePolicy("geolocation 'self'"); +} +} +---- + + [[headers-custom]] === Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. diff --git a/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java new file mode 100644 index 0000000000..1c5528b897 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 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 + * + * http://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 Feature + * Policy. + *

+ * Feature 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 feature policy contains a set of security policy directives, each + * responsible for declaring the restrictions for a particular feature type. + * + * @author Vedran Pavic + * @since 5.1 + */ +public final class FeaturePolicyHeaderWriter implements HeaderWriter { + + private static final String FEATURE_POLICY_HEADER = "Feature-Policy"; + + private String policyDirectives; + + /** + * Create a new instance of {@link FeaturePolicyHeaderWriter} with supplied security + * policy directive(s). + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyHeaderWriter(String policyDirectives) { + setPolicyDirectives(policyDirectives); + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + response.setHeader(FEATURE_POLICY_HEADER, this.policyDirectives); + } + + /** + * Set the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public void setPolicyDirectives(String policyDirectives) { + Assert.hasLength(policyDirectives, "policyDirectives must not be null or empty"); + this.policyDirectives = policyDirectives; + } + + @Override + public String toString() { + return getClass().getName() + " [policyDirectives=" + this.policyDirectives + "]"; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java new file mode 100644 index 0000000000..cf8b1fdecc --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.assertThatThrownBy; + +/** + * Tests for {@link FeaturePolicyHeaderWriter}. + * + * @author Vedran Pavic + */ +public class FeaturePolicyHeaderWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation 'self'"; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private FeaturePolicyHeaderWriter writer; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.writer = new FeaturePolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersFeaturePolicyDefault() { + writer.writeHeaders(this.request, this.response); + + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader("Feature-Policy")) + .isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void createWriterWithNullDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + + @Test + public void createWriterWithEmptyDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + +}