Skip to content

Commit

Permalink
Adds support for Content Security Policy.
Browse files Browse the repository at this point in the history
  • Loading branch information
jgrandja committed Mar 22, 2016
1 parent 533a5f0 commit 668154e
Show file tree
Hide file tree
Showing 9 changed files with 675 additions and 17 deletions.
Expand Up @@ -27,11 +27,7 @@
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;
Expand Down Expand Up @@ -59,6 +55,7 @@
*
* @author Rob Winch
* @author Tim Ysewyn
* @author Joe Grandja
* @since 3.2
*/
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
Expand All @@ -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
*
Expand Down Expand Up @@ -657,6 +656,64 @@ private HpkpConfig enable() {
}
}

/**
* <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
Expand Down Expand Up @@ -712,6 +769,7 @@ private List<HeaderWriter> getHeaderWriters() {
addIfNotNull(writers, hsts.writer);
addIfNotNull(writers, frameOptions.writer);
addIfNotNull(writers, hpkp.writer);
addIfNotNull(writers, contentSecurityPolicy.writer);
writers.addAll(headerWriters);
return writers;
}
Expand Down
Expand Up @@ -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";

Expand All @@ -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;
Expand All @@ -104,6 +107,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {

parseHpkpElement(element == null || !disabled, element, parserContext);

parseContentSecurityPolicyElement(disabled, element, parserContext);

parseHeaderElements(element);

if (disabled) {
Expand Down Expand Up @@ -258,6 +263,34 @@ private void addHpkp(boolean addIfNotPresent, Element hpkpElement, ParserContext
}
}

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(
Expand Down
Expand Up @@ -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}?
Expand Down Expand Up @@ -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}
Expand Down
Expand Up @@ -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"/>
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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("");
}
}

}
Expand Up @@ -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 ->
Expand Down

0 comments on commit 668154e

Please sign in to comment.