Adds support for Content Security Policy
Fixes gh-2342
This commit is contained in:
+63
-5
@@ -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<H extends HttpSecurityBuilder<H>> extends
|
||||
@@ -79,6 +76,8 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
|
||||
private final HpkpConfig hpkp = new HpkpConfig();
|
||||
|
||||
private final ContentSecurityPolicyConfig contentSecurityPolicy = new ContentSecurityPolicyConfig();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
@@ -657,6 +656,64 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Allows configuration for <a href="https://www.w3.org/TR/CSP2/">Content Security Policy (CSP) Level 2</a>.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Calling this method automatically enables (includes) the Content-Security-Policy header in the response
|
||||
* using the supplied security policy directive(s).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Configuration is provided to the {@link ContentSecurityPolicyHeaderWriter} which supports the writing
|
||||
* of the two headers as detailed in the W3C Candidate Recommendation:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Content-Security-Policy</li>
|
||||
* <li>Content-Security-Policy-Report-Only</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<H> 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<H extends HttpSecurityBuilder<H>> extends
|
||||
addIfNotNull(writers, hsts.writer);
|
||||
addIfNotNull(writers, frameOptions.writer);
|
||||
addIfNotNull(writers, hpkp.writer);
|
||||
addIfNotNull(writers, contentSecurityPolicy.writer);
|
||||
writers.addAll(headerWriters);
|
||||
return writers;
|
||||
}
|
||||
|
||||
+33
@@ -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<BeanMetadataElement> 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(
|
||||
|
||||
+11
-1
@@ -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}
|
||||
|
||||
@@ -2328,6 +2328,7 @@
|
||||
<xs:element ref="security:frame-options"/>
|
||||
<xs:element ref="security:content-type-options"/>
|
||||
<xs:element ref="security:hpkp"/>
|
||||
<xs:element ref="security:content-security-policy"/>
|
||||
<xs:element ref="security:header"/>
|
||||
</xs:choice>
|
||||
<xs:attributeGroup ref="security:headers-options.attlist"/>
|
||||
@@ -2460,6 +2461,31 @@
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="content-security-policy">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Adds support for Content Security Policy (CSP)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:attributeGroup ref="security:csp-options.attlist"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:attributeGroup name="csp-options.attlist">
|
||||
<xs:attribute name="policy-directives" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>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.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="report-only" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy
|
||||
violations only. Defaults to false.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="cache-control">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for
|
||||
|
||||
+66
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+78
@@ -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<String,String> expected) {
|
||||
assert response.headerNames == expected.keySet()
|
||||
expected.each { headerName, value ->
|
||||
|
||||
Reference in New Issue
Block a user