Skip to content

Commit 2f7f2ff

Browse files
jgrandjaRob Winch
authored andcommitted
Adds support for Content Security Policy
Fixes gh-2342
1 parent 4cb9b20 commit 2f7f2ff

9 files changed

Lines changed: 675 additions & 17 deletions

File tree

config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@
2727
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
2828
import org.springframework.security.web.header.HeaderWriter;
2929
import org.springframework.security.web.header.HeaderWriterFilter;
30-
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
31-
import org.springframework.security.web.header.writers.HpkpHeaderWriter;
32-
import org.springframework.security.web.header.writers.HstsHeaderWriter;
33-
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
34-
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
30+
import org.springframework.security.web.header.writers.*;
3531
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
3632
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode;
3733
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -59,6 +55,7 @@
5955
*
6056
* @author Rob Winch
6157
* @author Tim Ysewyn
58+
* @author Joe Grandja
6259
* @since 3.2
6360
*/
6461
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
@@ -79,6 +76,8 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
7976

8077
private final HpkpConfig hpkp = new HpkpConfig();
8178

79+
private final ContentSecurityPolicyConfig contentSecurityPolicy = new ContentSecurityPolicyConfig();
80+
8281
/**
8382
* Creates a new instance
8483
*
@@ -657,6 +656,64 @@ private HpkpConfig enable() {
657656
}
658657
}
659658

659+
/**
660+
* <p>
661+
* Allows configuration for <a href="https://www.w3.org/TR/CSP2/">Content Security Policy (CSP) Level 2</a>.
662+
* </p>
663+
*
664+
* <p>
665+
* Calling this method automatically enables (includes) the Content-Security-Policy header in the response
666+
* using the supplied security policy directive(s).
667+
* </p>
668+
*
669+
* <p>
670+
* Configuration is provided to the {@link ContentSecurityPolicyHeaderWriter} which supports the writing
671+
* of the two headers as detailed in the W3C Candidate Recommendation:
672+
* </p>
673+
* <ul>
674+
* <li>Content-Security-Policy</li>
675+
* <li>Content-Security-Policy-Report-Only</li>
676+
* </ul>
677+
*
678+
* @see ContentSecurityPolicyHeaderWriter
679+
* @since 4.1
680+
* @return the ContentSecurityPolicyConfig for additional configuration
681+
* @throws IllegalArgumentException if policyDirectives is null or empty
682+
*/
683+
public ContentSecurityPolicyConfig contentSecurityPolicy(String policyDirectives) {
684+
this.contentSecurityPolicy.writer =
685+
new ContentSecurityPolicyHeaderWriter(policyDirectives);
686+
return contentSecurityPolicy;
687+
}
688+
689+
public final class ContentSecurityPolicyConfig {
690+
private ContentSecurityPolicyHeaderWriter writer;
691+
692+
private ContentSecurityPolicyConfig() {
693+
}
694+
695+
/**
696+
* Enables (includes) the Content-Security-Policy-Report-Only header in the response.
697+
*
698+
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
699+
*/
700+
public ContentSecurityPolicyConfig reportOnly() {
701+
this.writer.setReportOnly(true);
702+
return this;
703+
}
704+
705+
/**
706+
* Allows completing configuration of Content Security Policy and continuing
707+
* configuration of headers.
708+
*
709+
* @return the {@link HeadersConfigurer} for additional configuration
710+
*/
711+
public HeadersConfigurer<H> and() {
712+
return HeadersConfigurer.this;
713+
}
714+
715+
}
716+
660717
/**
661718
* Clears all of the default headers from the response. After doing so, one can add
662719
* headers back. For example, if you only want to use Spring Security's cache control
@@ -712,6 +769,7 @@ private List<HeaderWriter> getHeaderWriters() {
712769
addIfNotNull(writers, hsts.writer);
713770
addIfNotNull(writers, frameOptions.writer);
714771
addIfNotNull(writers, hpkp.writer);
772+
addIfNotNull(writers, contentSecurityPolicy.writer);
715773
writers.addAll(headerWriters);
716774
return writers;
717775
}

config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
6767
private static final String ATT_REPORT_ONLY = "report-only";
6868
private static final String ATT_REPORT_URI = "report-uri";
6969
private static final String ATT_ALGORITHM = "algorithm";
70+
private static final String ATT_POLICY_DIRECTIVES = "policy-directives";
7071

7172
private static final String CACHE_CONTROL_ELEMENT = "cache-control";
7273

@@ -80,6 +81,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
8081
private static final String FRAME_OPTIONS_ELEMENT = "frame-options";
8182
private static final String GENERIC_HEADER_ELEMENT = "header";
8283

84+
private static final String CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy";
85+
8386
private static final String ALLOW_FROM = "ALLOW-FROM";
8487

8588
private ManagedList<BeanMetadataElement> headerWriters;
@@ -104,6 +107,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
104107

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

110+
parseContentSecurityPolicyElement(disabled, element, parserContext);
111+
107112
parseHeaderElements(element);
108113

109114
if (disabled) {
@@ -258,6 +263,34 @@ private void addHpkp(boolean addIfNotPresent, Element hpkpElement, ParserContext
258263
}
259264
}
260265

266+
private void parseContentSecurityPolicyElement(boolean elementDisabled, Element element, ParserContext context) {
267+
Element contentSecurityPolicyElement = (elementDisabled || element == null) ? null : DomUtils.getChildElementByTagName(
268+
element, CONTENT_SECURITY_POLICY_ELEMENT);
269+
if (contentSecurityPolicyElement != null) {
270+
addContentSecurityPolicy(contentSecurityPolicyElement, context);
271+
}
272+
}
273+
274+
private void addContentSecurityPolicy(Element contentSecurityPolicyElement, ParserContext context) {
275+
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder
276+
.genericBeanDefinition(ContentSecurityPolicyHeaderWriter.class);
277+
278+
String policyDirectives = contentSecurityPolicyElement.getAttribute(ATT_POLICY_DIRECTIVES);
279+
if (!StringUtils.hasText(policyDirectives)) {
280+
context.getReaderContext().error(
281+
ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", contentSecurityPolicyElement);
282+
} else {
283+
headersWriter.addConstructorArgValue(policyDirectives);
284+
}
285+
286+
String reportOnly = contentSecurityPolicyElement.getAttribute(ATT_REPORT_ONLY);
287+
if (StringUtils.hasText(reportOnly)) {
288+
headersWriter.addPropertyValue("reportOnly", reportOnly);
289+
}
290+
291+
headerWriters.add(headersWriter.getBeanDefinition());
292+
}
293+
261294
private void attrNotAllowed(ParserContext context, String attrName,
262295
String otherAttrName, Element element) {
263296
context.getReaderContext().error(

config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ csrf-options.attlist &=
748748

749749
headers =
750750
## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
751-
element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & header*)}
751+
element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & header*)}
752752
headers-options.attlist &=
753753
## Specifies if the default headers should be disabled. Default false.
754754
attribute defaults-disabled {xsd:boolean}?
@@ -800,6 +800,16 @@ hpkp.attlist &=
800800
## Specifies the URI to which the browser should report pin validation failures.
801801
attribute report-uri {xsd:string}?
802802

803+
content-security-policy =
804+
## Adds support for Content Security Policy (CSP)
805+
element content-security-policy {csp-options.attlist}
806+
csp-options.attlist &=
807+
## 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.
808+
attribute policy-directives {xsd:token}?
809+
csp-options.attlist &=
810+
## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false.
811+
attribute report-only {xsd:boolean}?
812+
803813
cache-control =
804814
## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request
805815
element cache-control {cache-control.attlist}

config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,6 +2328,7 @@
23282328
<xs:element ref="security:frame-options"/>
23292329
<xs:element ref="security:content-type-options"/>
23302330
<xs:element ref="security:hpkp"/>
2331+
<xs:element ref="security:content-security-policy"/>
23312332
<xs:element ref="security:header"/>
23322333
</xs:choice>
23332334
<xs:attributeGroup ref="security:headers-options.attlist"/>
@@ -2460,6 +2461,31 @@
24602461
</xs:annotation>
24612462
</xs:attribute>
24622463
</xs:attributeGroup>
2464+
<xs:element name="content-security-policy">
2465+
<xs:annotation>
2466+
<xs:documentation>Adds support for Content Security Policy (CSP)
2467+
</xs:documentation>
2468+
</xs:annotation>
2469+
<xs:complexType>
2470+
<xs:attributeGroup ref="security:csp-options.attlist"/>
2471+
</xs:complexType>
2472+
</xs:element>
2473+
<xs:attributeGroup name="csp-options.attlist">
2474+
<xs:attribute name="policy-directives" type="xs:token">
2475+
<xs:annotation>
2476+
<xs:documentation>The security policy directive(s) for the Content-Security-Policy header or if report-only
2477+
is set to true, then the Content-Security-Policy-Report-Only header is used.
2478+
</xs:documentation>
2479+
</xs:annotation>
2480+
</xs:attribute>
2481+
<xs:attribute name="report-only" type="xs:boolean">
2482+
<xs:annotation>
2483+
<xs:documentation>Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy
2484+
violations only. Defaults to false.
2485+
</xs:documentation>
2486+
</xs:annotation>
2487+
</xs:attribute>
2488+
</xs:attributeGroup>
24632489
<xs:element name="cache-control">
24642490
<xs:annotation>
24652491
<xs:documentation>Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configurers
1717

18+
import org.springframework.beans.factory.BeanCreationException
1819
import org.springframework.security.config.annotation.BaseSpringSpec
1920
import org.springframework.security.config.annotation.web.builders.HttpSecurity
2021
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@@ -24,6 +25,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
2425
*
2526
* @author Rob Winch
2627
* @author Tim Ysewyn
28+
* @author Joe Grandja
2729
*/
2830
class HeadersConfigurerTests extends BaseSpringSpec {
2931

@@ -387,4 +389,68 @@ class HeadersConfigurerTests extends BaseSpringSpec {
387389
.reportUri("http://example.net/pkp-report")
388390
}
389391
}
392+
393+
def "headers.contentSecurityPolicy default header"() {
394+
setup:
395+
loadConfig(ContentSecurityPolicyDefaultConfig)
396+
request.secure = true
397+
when:
398+
springSecurityFilterChain.doFilter(request,response,chain)
399+
then:
400+
responseHeaders == ['Content-Security-Policy': 'default-src \'self\'']
401+
}
402+
403+
@EnableWebSecurity
404+
static class ContentSecurityPolicyDefaultConfig extends WebSecurityConfigurerAdapter {
405+
406+
@Override
407+
protected void configure(HttpSecurity http) throws Exception {
408+
http
409+
.headers()
410+
.defaultsDisabled()
411+
.contentSecurityPolicy("default-src 'self'");
412+
}
413+
}
414+
415+
def "headers.contentSecurityPolicy report-only header"() {
416+
setup:
417+
loadConfig(ContentSecurityPolicyReportOnlyConfig)
418+
request.secure = true
419+
when:
420+
springSecurityFilterChain.doFilter(request,response,chain)
421+
then:
422+
responseHeaders == ['Content-Security-Policy-Report-Only': 'default-src \'self\'; script-src trustedscripts.example.com']
423+
}
424+
425+
@EnableWebSecurity
426+
static class ContentSecurityPolicyReportOnlyConfig extends WebSecurityConfigurerAdapter {
427+
428+
@Override
429+
protected void configure(HttpSecurity http) throws Exception {
430+
http
431+
.headers()
432+
.defaultsDisabled()
433+
.contentSecurityPolicy("default-src 'self'; script-src trustedscripts.example.com").reportOnly();
434+
}
435+
}
436+
437+
def "headers.contentSecurityPolicy empty policyDirectives"() {
438+
when:
439+
loadConfig(ContentSecurityPolicyInvalidConfig)
440+
then:
441+
thrown(BeanCreationException)
442+
}
443+
444+
@EnableWebSecurity
445+
static class ContentSecurityPolicyInvalidConfig extends WebSecurityConfigurerAdapter {
446+
447+
@Override
448+
protected void configure(HttpSecurity http) throws Exception {
449+
http
450+
.headers()
451+
.defaultsDisabled()
452+
.contentSecurityPolicy("");
453+
}
454+
}
455+
390456
}

config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,84 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
830830
expected.message.contains 'policy'
831831
}
832832

833+
def 'http headers defaults : content-security-policy'() {
834+
setup:
835+
httpAutoConfig {
836+
'headers'() {
837+
'content-security-policy'('policy-directives':'default-src \'self\'')
838+
}
839+
}
840+
createAppContext()
841+
when:
842+
def hf = getFilter(HeaderWriterFilter)
843+
MockHttpServletResponse response = new MockHttpServletResponse()
844+
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
845+
def expectedHeaders = [:] << defaultHeaders
846+
expectedHeaders['Content-Security-Policy'] = 'default-src \'self\''
847+
then:
848+
assertHeaders(response, expectedHeaders)
849+
}
850+
851+
def 'http headers disabled : content-security-policy not included'() {
852+
setup:
853+
httpAutoConfig {
854+
'headers'(disabled:true) {
855+
'content-security-policy'('policy-directives':'default-src \'self\'')
856+
}
857+
}
858+
createAppContext()
859+
when:
860+
def hf = getFilter(HeaderWriterFilter)
861+
then:
862+
!hf
863+
}
864+
865+
def 'http headers defaults disabled : content-security-policy only'() {
866+
setup:
867+
httpAutoConfig {
868+
'headers'('defaults-disabled':true) {
869+
'content-security-policy'('policy-directives':'default-src \'self\'')
870+
}
871+
}
872+
createAppContext()
873+
when:
874+
def hf = getFilter(HeaderWriterFilter)
875+
MockHttpServletResponse response = new MockHttpServletResponse()
876+
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
877+
then:
878+
assertHeaders(response, ['Content-Security-Policy':'default-src \'self\''])
879+
}
880+
881+
def 'http headers defaults : content-security-policy with empty directives'() {
882+
when:
883+
httpAutoConfig {
884+
'headers'() {
885+
'content-security-policy'('policy-directives':'')
886+
}
887+
}
888+
createAppContext()
889+
then:
890+
thrown(BeanDefinitionParsingException)
891+
}
892+
893+
def 'http headers defaults : content-security-policy report-only=true'() {
894+
setup:
895+
httpAutoConfig {
896+
'headers'() {
897+
'content-security-policy'('policy-directives':'default-src https:; report-uri https://example.com/', 'report-only':true)
898+
}
899+
}
900+
createAppContext()
901+
when:
902+
def hf = getFilter(HeaderWriterFilter)
903+
MockHttpServletResponse response = new MockHttpServletResponse()
904+
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
905+
def expectedHeaders = [:] << defaultHeaders
906+
expectedHeaders['Content-Security-Policy-Report-Only'] = 'default-src https:; report-uri https://example.com/'
907+
then:
908+
assertHeaders(response, expectedHeaders)
909+
}
910+
833911
def assertHeaders(MockHttpServletResponse response, Map<String,String> expected) {
834912
assert response.headerNames == expected.keySet()
835913
expected.each { headerName, value ->

0 commit comments

Comments
 (0)