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 621e90b94b..dd6726bdad 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 @@ -27,11 +27,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; -import org.springframework.security.web.header.writers.CacheControlHeadersWriter; -import org.springframework.security.web.header.writers.HpkpHeaderWriter; -import org.springframework.security.web.header.writers.HstsHeaderWriter; -import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter; -import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; +import org.springframework.security.web.header.writers.*; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -59,6 +55,7 @@ import org.springframework.util.Assert; * * @author Rob Winch * @author Tim Ysewyn + * @author Joe Grandja * @since 3.2 */ public class HeadersConfigurer> extends @@ -79,6 +76,8 @@ public class HeadersConfigurer> extends private final HpkpConfig hpkp = new HpkpConfig(); + private final ContentSecurityPolicyConfig contentSecurityPolicy = new ContentSecurityPolicyConfig(); + /** * Creates a new instance * @@ -657,6 +656,64 @@ public class HeadersConfigurer> extends } } + /** + *

+ * Allows configuration for Content Security Policy (CSP) Level 2. + *

+ * + *

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

+ * + *

+ * Configuration is provided to the {@link ContentSecurityPolicyHeaderWriter} which supports the writing + * of the two headers as detailed in the W3C Candidate Recommendation: + *

+ *
    + *
  • Content-Security-Policy
  • + *
  • Content-Security-Policy-Report-Only
  • + *
+ * + * @see ContentSecurityPolicyHeaderWriter + * @since 4.1 + * @return the ContentSecurityPolicyConfig for additional configuration + * @throws IllegalArgumentException if policyDirectives is null or empty + */ + public ContentSecurityPolicyConfig contentSecurityPolicy(String policyDirectives) { + this.contentSecurityPolicy.writer = + new ContentSecurityPolicyHeaderWriter(policyDirectives); + return contentSecurityPolicy; + } + + public final class ContentSecurityPolicyConfig { + private ContentSecurityPolicyHeaderWriter writer; + + private ContentSecurityPolicyConfig() { + } + + /** + * Enables (includes) the Content-Security-Policy-Report-Only header in the response. + * + * @return the {@link ContentSecurityPolicyConfig} for additional configuration + */ + public ContentSecurityPolicyConfig reportOnly() { + this.writer.setReportOnly(true); + return this; + } + + /** + * Allows completing configuration of Content Security Policy and continuing + * configuration of headers. + * + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + /** * Clears all of the default headers from the response. After doing so, one can add * headers back. For example, if you only want to use Spring Security's cache control @@ -712,6 +769,7 @@ public class HeadersConfigurer> extends addIfNotNull(writers, hsts.writer); addIfNotNull(writers, frameOptions.writer); addIfNotNull(writers, hpkp.writer); + addIfNotNull(writers, contentSecurityPolicy.writer); writers.addAll(headerWriters); return writers; } 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 7300a42f7b..d75b5a0169 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 @@ -67,6 +67,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String ATT_REPORT_ONLY = "report-only"; private static final String ATT_REPORT_URI = "report-uri"; private static final String ATT_ALGORITHM = "algorithm"; + private static final String ATT_POLICY_DIRECTIVES = "policy-directives"; private static final String CACHE_CONTROL_ELEMENT = "cache-control"; @@ -80,6 +81,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String FRAME_OPTIONS_ELEMENT = "frame-options"; private static final String GENERIC_HEADER_ELEMENT = "header"; + private static final String CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy"; + private static final String ALLOW_FROM = "ALLOW-FROM"; private ManagedList headerWriters; @@ -104,6 +107,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { parseHpkpElement(element == null || !disabled, element, parserContext); + parseContentSecurityPolicyElement(disabled, element, parserContext); + parseHeaderElements(element); if (disabled) { @@ -258,6 +263,34 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { } } + private void parseContentSecurityPolicyElement(boolean elementDisabled, Element element, ParserContext context) { + Element contentSecurityPolicyElement = (elementDisabled || element == null) ? null : DomUtils.getChildElementByTagName( + element, CONTENT_SECURITY_POLICY_ELEMENT); + if (contentSecurityPolicyElement != null) { + addContentSecurityPolicy(contentSecurityPolicyElement, context); + } + } + + private void addContentSecurityPolicy(Element contentSecurityPolicyElement, ParserContext context) { + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder + .genericBeanDefinition(ContentSecurityPolicyHeaderWriter.class); + + String policyDirectives = contentSecurityPolicyElement.getAttribute(ATT_POLICY_DIRECTIVES); + if (!StringUtils.hasText(policyDirectives)) { + context.getReaderContext().error( + ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", contentSecurityPolicyElement); + } else { + headersWriter.addConstructorArgValue(policyDirectives); + } + + String reportOnly = contentSecurityPolicyElement.getAttribute(ATT_REPORT_ONLY); + if (StringUtils.hasText(reportOnly)) { + headersWriter.addPropertyValue("reportOnly", reportOnly); + } + + 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-4.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc index 88fb85d780..ea85a0fb26 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc @@ -748,7 +748,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? & header*)} +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & header*)} headers-options.attlist &= ## Specifies if the default headers should be disabled. Default false. attribute defaults-disabled {xsd:boolean}? @@ -800,6 +800,16 @@ hpkp.attlist &= ## Specifies the URI to which the browser should report pin validation failures. attribute report-uri {xsd:string}? +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + 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-4.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd index 90ef08ab80..b642140e04 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd @@ -2328,6 +2328,7 @@ + @@ -2460,6 +2461,31 @@ + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for 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 1c924f7874..e39efaacb6 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 @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers +import org.springframework.beans.factory.BeanCreationException 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 @@ -24,6 +25,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur * * @author Rob Winch * @author Tim Ysewyn + * @author Joe Grandja */ class HeadersConfigurerTests extends BaseSpringSpec { @@ -387,4 +389,68 @@ class HeadersConfigurerTests extends BaseSpringSpec { .reportUri("http://example.net/pkp-report") } } + + def "headers.contentSecurityPolicy default header"() { + setup: + loadConfig(ContentSecurityPolicyDefaultConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['Content-Security-Policy': 'default-src \'self\''] + } + + @EnableWebSecurity + static class ContentSecurityPolicyDefaultConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .contentSecurityPolicy("default-src 'self'"); + } + } + + def "headers.contentSecurityPolicy report-only header"() { + setup: + loadConfig(ContentSecurityPolicyReportOnlyConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['Content-Security-Policy-Report-Only': 'default-src \'self\'; script-src trustedscripts.example.com'] + } + + @EnableWebSecurity + static class ContentSecurityPolicyReportOnlyConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .contentSecurityPolicy("default-src 'self'; script-src trustedscripts.example.com").reportOnly(); + } + } + + def "headers.contentSecurityPolicy empty policyDirectives"() { + when: + loadConfig(ContentSecurityPolicyInvalidConfig) + then: + thrown(BeanCreationException) + } + + @EnableWebSecurity + static class ContentSecurityPolicyInvalidConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .contentSecurityPolicy(""); + } + } + } diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy index c692ad83b3..8c3c526373 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy @@ -830,6 +830,84 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { expected.message.contains 'policy' } + def 'http headers defaults : content-security-policy'() { + setup: + httpAutoConfig { + 'headers'() { + 'content-security-policy'('policy-directives':'default-src \'self\'') + } + } + createAppContext() + when: + def hf = getFilter(HeaderWriterFilter) + MockHttpServletResponse response = new MockHttpServletResponse() + hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + def expectedHeaders = [:] << defaultHeaders + expectedHeaders['Content-Security-Policy'] = 'default-src \'self\'' + then: + assertHeaders(response, expectedHeaders) + } + + def 'http headers disabled : content-security-policy not included'() { + setup: + httpAutoConfig { + 'headers'(disabled:true) { + 'content-security-policy'('policy-directives':'default-src \'self\'') + } + } + createAppContext() + when: + def hf = getFilter(HeaderWriterFilter) + then: + !hf + } + + def 'http headers defaults disabled : content-security-policy only'() { + setup: + httpAutoConfig { + 'headers'('defaults-disabled':true) { + 'content-security-policy'('policy-directives':'default-src \'self\'') + } + } + createAppContext() + when: + def hf = getFilter(HeaderWriterFilter) + MockHttpServletResponse response = new MockHttpServletResponse() + hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + then: + assertHeaders(response, ['Content-Security-Policy':'default-src \'self\'']) + } + + def 'http headers defaults : content-security-policy with empty directives'() { + when: + httpAutoConfig { + 'headers'() { + 'content-security-policy'('policy-directives':'') + } + } + createAppContext() + then: + thrown(BeanDefinitionParsingException) + } + + def 'http headers defaults : content-security-policy report-only=true'() { + setup: + httpAutoConfig { + 'headers'() { + 'content-security-policy'('policy-directives':'default-src https:; report-uri https://example.com/', 'report-only':true) + } + } + createAppContext() + when: + def hf = getFilter(HeaderWriterFilter) + MockHttpServletResponse response = new MockHttpServletResponse() + hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) + def expectedHeaders = [:] << defaultHeaders + expectedHeaders['Content-Security-Policy-Report-Only'] = 'default-src https:; report-uri https://example.com/' + then: + assertHeaders(response, expectedHeaders) + } + def assertHeaders(MockHttpServletResponse response, Map expected) { assert response.headerNames == expected.keySet() expected.each { headerName, value -> diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index a94fb3d274..0e237a102c 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -3825,7 +3825,7 @@ Allowing your website to be added to a frame can be a security issue. For exampl [NOTE] ==== -Another modern approach to dealing with clickjacking is using a http://www.w3.org/TR/CSP/[Content Security Policy]. Spring Security does not provide support for this as the specification is not released and it is quite a bit more complicated. However, you could use the <> feature to implement this. To stay up to date with this issue and to see how you can implement it with Spring Security refer to https://jira.spring.io/browse/SEC-2117[SEC-2117] +Another modern approach to dealing with clickjacking is to use <>. ==== There are a number ways to mitigate clickjacking attacks. For example, to protect legacy browsers from clickjacking attacks you can use https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet#Best-for-now_Legacy_Browser_Frame_Breaking_Script[frame breaking code]. While not perfect, the frame breaking code is the best you can do for the legacy browsers. @@ -3917,6 +3917,153 @@ protected void configure(HttpSecurity http) throws Exception { } ---- +[[headers-content-security-policy]] +==== Content Security Policy (CSP) + +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). CSP is a declarative policy that provides a facility for web application authors to declare and ultimately inform +the client (user-agent) about the sources from which the web application expects to load resources. + +[NOTE] +==== +Content Security Policy is not intended to solve all content injection vulnerabilities. +Instead, CSP can be leveraged to help reduce the harm caused by content injection attacks. +As a first line of defense, web application authors should validate their input and encode their output. +==== + +A web application may employ the use of CSP by including one of the following HTTP headers in the response: + +* *_Content-Security-Policy_* +* *_Content-Security-Policy-Report-Only_* + +Each of these headers are used as a mechanism to deliver a *_security policy_* to the client. +A security policy contains a set of *_security policy directives_* (for example, _script-src_ and _object-src_), +each responsible for declaring the restrictions for a particular resource representation. + +For example, a web application can declare that it expects to load scripts from specific, trusted sources, +by including the following header in the response: + +[source] +---- +Content-Security-Policy: script-src https://trustedscripts.example.com +---- + +An attempt to load a script from another source other than what is declared in the _script-src_ directive will be blocked by the user-agent. +Additionally, if the https://www.w3.org/TR/CSP2/#directive-report-uri[*_report-uri_*] directive is declared in the security policy, +then the violation will be reported by the user-agent to the declared URL. + +For example, if a web application violates the declared security policy, +the following response header will instruct the user-agent to send violation reports to the URL specified in the policy’s _report-uri_ directive. + +[source] +---- +Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/ +---- + +The *_Content-Security-Policy-Report-Only_* header provides the capability for web application authors and administrators to monitor security policies, rather than enforce them. +This header is typically used when experimenting and/or developing security policies for a site. +When a policy is deemed effective, it can be enforced by using the _Content-Security-Policy_ header field instead. + +Given the following response header, the policy declares that scripts may be loaded from one of two possible sources. + +[source] +---- +Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/ +---- + +If the site violates this policy, by attempting to load a script from _evil.com_, +the user-agent will send a violation report to the declared URL specified by the _report-uri_ directive, +but still allow the violating resource to load nevertheless. + +===== Configuring Content Security Policy + +It's important to note that Spring Security *_does not add_* Content Security Policy by default. +The web application author must declare the security policy(s) to enforce and/or monitor for the protected resources. + +For example, given the following security policy: + +[source] +---- +script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/ +---- + +You can enable the CSP header using XML configuration with the <>> element as shown below: + +[source,xml] +---- + + + + + + + +---- + +To enable the CSP _'report-only'_ header, configure the element as follows: + +[source,xml] +---- + + + + + + + +---- + +Similarly, you can enable the CSP 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() + .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"); +} +} +---- + +To enable the CSP _'report-only'_ header, provide the following Java configuration: + +[source,java] +---- +@EnableWebSecurity +public class WebSecurityConfig extends +WebSecurityConfigurerAdapter { + +@Override +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .headers() + .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") + .reportOnly(); +} +} +---- + +===== Additional Resources + +Applying Content Security Policy to a web application is often a non-trivial undertaking. +The following resources may provide further assistance in developing effective security policies for your site. + +http://www.html5rocks.com/en/tutorials/security/content-security-policy/[An Introduction to Content Security Policy] + +https://developer.mozilla.org/en-US/docs/Web/Security/CSP[CSP Guide - Mozilla Developer Network] + +https://www.w3.org/TR/CSP2/[W3C Candidate Recommendation] + [[headers-custom]] === Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. However, it also provides hooks to enable adding custom headers. @@ -3924,15 +4071,11 @@ Spring Security has mechanisms to make it convenient to add the more common secu [[headers-static]] ==== Static Headers There may be times you wish to inject custom security headers into your application that are not supported out of the box. -For example, perhaps you wish to have early support for http://www.w3.org/TR/CSP/[Content Security Policy] in order to ensure that resources are only loaded from the same origin. -Since support for Content Security Policy has not been finalized, browsers use one of two common extension headers to implement the feature. -This means we will need to inject the policy twice. -An example of the headers can be seen below: +For example, given the following custom security header: [source] ---- -X-Content-Security-Policy: default-src 'self' -X-WebKit-CSP: default-src 'self' +X-Custom-Security-Header: header-value ---- When using the XML namespace, these headers can be added to the response using the <>> element as shown below: @@ -3943,8 +4086,7 @@ When using the XML namespace, these headers can be added to the response using t -
-
+
---- @@ -3962,8 +4104,7 @@ protected void configure(HttpSecurity http) throws Exception { http // ... .headers() - .addHeaderWriter(new StaticHeadersWriter("X-Content-Security-Policy","default-src 'self'")) - .addHeaderWriter(new StaticHeadersWriter("X-WebKit-CSP","default-src 'self'")); + .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value")); } } ---- @@ -7076,6 +7217,7 @@ This element allows for configuring additional (security) headers to be send wit ** `X-XSS-Protection` - Can be set using the <> element. The http://en.wikipedia.org/wiki/Cross-site_scripting[X-XSS-Protection ] header can be used by browser to do basic control. ** `X-Content-Type-Options` - Can be set using the <> element. The http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx[X-Content-Type-Options] header prevents Internet Explorer from MIME-sniffing a response away from the declared content-type. This also applies to Google Chrome, when downloading extensions. ** `Public-Key-Pinning` or `Public-Key-Pinning-Report-Only` - Can be set using the <> element. This allows HTTPS websites to resist impersonation by attackers using mis-issued or otherwise fraudulent certificates. +** `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). [[nsa-headers-attributes]] ===== Attributes @@ -7103,6 +7245,7 @@ Optional attribute that specifies to disable Spring Security's HTTP response hea * <> +* <> * <> * <> * <> @@ -7235,6 +7378,28 @@ The cryptographic hash algorithm. Default is SHA256. +[[nsa-content-security-policy]] +==== +When enabled adds the https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] header to the response. CSP is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). + +[[nsa-content-security-policy-attributes]] +===== Attributes + +[[nsa-content-security-policy-policy-directives]] +* **policy-directives** +The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + +[[nsa-content-security-policy-report-only]] +* **report-only** +Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + +[[nsa-content-security-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/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java new file mode 100644 index 0000000000..82d348d890 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2016 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.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * Provides support for Content Security Policy (CSP) Level 2. + *

+ * + *

+ * CSP provides a mechanism for web applications to mitigate content injection vulnerabilities, + * such as cross-site scripting (XSS). CSP is a declarative policy that allows web application authors to inform + * the client (user-agent) about the sources from which the application expects to load resources. + *

+ * + *

+ * For example, a web application can declare that it only expects to load script from specific, trusted sources. + * This declaration allows the client to detect and block malicious scripts injected into the application by an attacker. + *

+ * + *

+ * A declaration of a security policy contains a set of security policy directives (for example, script-src and object-src), + * each responsible for declaring the restrictions for a particular resource type. + * The list of directives defined can be found at + * Directives. + *

+ * + *

+ * Each directive has a name and value. For detailed syntax on writing security policies, see + * Syntax and Algorithms. + *

+ * + *

+ * This implementation of {@link HeaderWriter} writes one of the following headers: + *

+ *
    + *
  • Content-Security-Policy
  • + *
  • Content-Security-Policy-Report-Only
  • + *
+ * + *

+ * By default, the Content-Security-Policy header is included in the response. + * However, calling {@link #setReportOnly(boolean)} with {@code true} will include the + * Content-Security-Policy-Report-Only header in the response. + * NOTE: The supplied security policy directive(s) will be used for whichever header is enabled (included). + *

+ * + *

+ * + * CSP is not intended as a first line of defense against content injection vulnerabilities. + * Instead, CSP is used to reduce the harm caused by content injection attacks. + * As a first line of defense against content injection, web application authors should validate their input and encode their output. + * + *

+ * + * @author Joe Grandja + * @since 4.1 + */ +public final class ContentSecurityPolicyHeaderWriter implements HeaderWriter { + private static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy"; + + private static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER = "Content-Security-Policy-Report-Only"; + + private String policyDirectives; + + private boolean reportOnly; + + /** + * Creates a new instance + * + * @param policyDirectives maps to {@link #setPolicyDirectives(String)} + * @throws IllegalArgumentException if policyDirectives is null or empty + */ + public ContentSecurityPolicyHeaderWriter(String policyDirectives) { + setPolicyDirectives(policyDirectives); + this.reportOnly = false; + } + + /** + * @see org.springframework.security.web.header.HeaderWriter#writeHeaders(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + response.setHeader((!reportOnly ? CONTENT_SECURITY_POLICY_HEADER : CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER), policyDirectives); + } + + /** + * Sets the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is null or empty + */ + public void setPolicyDirectives(String policyDirectives) { + Assert.hasLength(policyDirectives, "policyDirectives cannot be null or empty"); + this.policyDirectives = policyDirectives; + } + + /** + * If true, includes the Content-Security-Policy-Report-Only header in the response, + * otherwise, defaults to the Content-Security-Policy header. + + * @param reportOnly set to true for reporting policy violations only + */ + public void setReportOnly(boolean reportOnly) { + this.reportOnly = reportOnly; + } + + @Override + public String toString() { + return getClass().getName() + " [policyDirectives=" + policyDirectives + "; reportOnly=" + reportOnly + "]"; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java new file mode 100644 index 0000000000..c73f7a6806 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 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; + +/** + * @author Joe Grandja + */ +public class ContentSecurityPolicyHeaderWriterTests { + private static final String DEFAULT_POLICY_DIRECTIVES = "default-src 'self'"; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private ContentSecurityPolicyHeaderWriter writer; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + request.setSecure(true); + response = new MockHttpServletResponse(); + writer = new ContentSecurityPolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersContentSecurityPolicyDefault() { + writer.writeHeaders(request, response); + + assertThat(response.getHeaderNames().size()).isEqualTo(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersContentSecurityPolicyCustom() { + String policyDirectives = "default-src 'self'; " + + "object-src plugins1.example.com plugins2.example.com; " + + "script-src trustedscripts.example.com"; + + writer = new ContentSecurityPolicyHeaderWriter(policyDirectives); + writer.writeHeaders(request, response); + + assertThat(response.getHeaderNames().size()).isEqualTo(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(policyDirectives); + } + + @Test + public void writeHeadersContentSecurityPolicyReportOnlyDefault() { + writer.setReportOnly(true); + writer.writeHeaders(request, response); + + assertThat(response.getHeaderNames().size()).isEqualTo(1); + assertThat(response.getHeader("Content-Security-Policy-Report-Only")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersContentSecurityPolicyReportOnlyCustom() { + String policyDirectives = "default-src https:; report-uri https://example.com/"; + + writer = new ContentSecurityPolicyHeaderWriter(policyDirectives); + writer.setReportOnly(true); + writer.writeHeaders(request, response); + + assertThat(response.getHeaderNames().size()).isEqualTo(1); + assertThat(response.getHeader("Content-Security-Policy-Report-Only")).isEqualTo(policyDirectives); + } + + @Test(expected = IllegalArgumentException.class) + public void writeHeadersContentSecurityPolicyInvalid() { + writer = new ContentSecurityPolicyHeaderWriter(""); + writer = new ContentSecurityPolicyHeaderWriter(null); + } + +} \ No newline at end of file