From d7bbea63b77d55db0db34f9f488ac6e51debdcfa Mon Sep 17 00:00:00 2001 From: Christoffer Sawicki Date: Sat, 12 Nov 2016 20:36:54 +0100 Subject: [PATCH] Add properties for content security policy Add `content-security-policy` and `content-security-policy-mode` `security.header` properties and update auto-configuration to apply them when specified. Fixes gh-7373 Closes gh-7357 --- .../security/SecurityProperties.java | 38 +++++++++++++++++++ .../SpringBootWebSecurityConfiguration.java | 9 +++++ ...ringBootWebSecurityConfigurationTests.java | 34 ++++++++++++++++- .../appendix-application-properties.adoc | 2 + 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java index 6126f61966..97d2ad4795 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java @@ -174,6 +174,18 @@ public class SecurityProperties implements SecurityPrerequisite { NONE, DOMAIN, ALL } + public enum ContentSecurityPolicyMode { + /** + * Use the {@code Content-Security-Policy} header. + */ + DEFAULT, + + /** + * Use the {@code Content-Security-Policy-Report-Only} header. + */ + REPORT_ONLY + } + /** * Enable cross site scripting (XSS) protection. */ @@ -194,6 +206,16 @@ public class SecurityProperties implements SecurityPrerequisite { */ private boolean contentType = true; + /** + * Value for content security policy header. + */ + private String contentSecurityPolicy; + + /** + * Whether to use the "Content-Security-Policy" or "Content-Security-Policy-Report-Only" header. + */ + private ContentSecurityPolicyMode contentSecurityPolicyMode = ContentSecurityPolicyMode.DEFAULT; + /** * HTTP Strict Transport Security (HSTS) mode (none, domain, all). */ @@ -231,6 +253,22 @@ public class SecurityProperties implements SecurityPrerequisite { this.contentType = contentType; } + public String getContentSecurityPolicy() { + return this.contentSecurityPolicy; + } + + public void setContentSecurityPolicy(String contentSecurityPolicy) { + this.contentSecurityPolicy = contentSecurityPolicy; + } + + public ContentSecurityPolicyMode getContentSecurityPolicyMode() { + return this.contentSecurityPolicyMode; + } + + public void setContentSecurityPolicyMode(ContentSecurityPolicyMode contentSecurityPolicyMode) { + this.contentSecurityPolicyMode = contentSecurityPolicyMode; + } + public HSTS getHsts() { return this.hsts; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java index 22747e1ad7..daa5e3569a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java @@ -108,6 +108,15 @@ public class SpringBootWebSecurityConfiguration { if (!headers.isContentType()) { configurer.contentTypeOptions().disable(); } + if (StringUtils.hasText(headers.getContentSecurityPolicy())) { + if (headers.getContentSecurityPolicyMode() == Headers.ContentSecurityPolicyMode.DEFAULT) { + configurer.contentSecurityPolicy(headers.getContentSecurityPolicy()); + } + else { + assert headers.getContentSecurityPolicyMode() == Headers.ContentSecurityPolicyMode.REPORT_ONLY; + configurer.contentSecurityPolicy(headers.getContentSecurityPolicy()).reportOnly(); + } + } if (!headers.isXss()) { configurer.xssProtection().disable(); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java index 82d6956546..23af97a970 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java @@ -213,7 +213,8 @@ public class SpringBootWebSecurityConfigurationTests { .andExpect(MockMvcResultMatchers.header().string("Cache-Control", is(notNullValue()))) .andExpect(MockMvcResultMatchers.header().string("X-Frame-Options", - is(notNullValue()))); + is(notNullValue()))) + .andExpect(MockMvcResultMatchers.header().doesNotExist("Content-Security-Policy")); } @Test @@ -239,6 +240,37 @@ public class SpringBootWebSecurityConfigurationTests { MockMvcResultMatchers.header().doesNotExist("X-Frame-Options")); } + @Test + public void contentSecurityPolicyConfiguration() throws Exception { + this.context = SpringApplication.run(VanillaWebConfiguration.class, + "--security.headers.content-security-policy=default-src 'self';"); + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup((WebApplicationContext) this.context) + .addFilters((FilterChainProxy) this.context + .getBean("springSecurityFilterChain", Filter.class)) + .build(); + mockMvc.perform(MockMvcRequestBuilders.get("/")) + .andExpect(MockMvcResultMatchers.header().string("Content-Security-Policy", + is("default-src 'self';"))) + .andExpect(MockMvcResultMatchers.header().doesNotExist("Content-Security-Policy-Report-Only")); + } + + @Test + public void contentSecurityPolicyReportOnlyConfiguration() throws Exception { + this.context = SpringApplication.run(VanillaWebConfiguration.class, + "--security.headers.content-security-policy=default-src 'self';", + "--security.headers.content-security-policy-mode=report-only"); + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup((WebApplicationContext) this.context) + .addFilters((FilterChainProxy) this.context + .getBean("springSecurityFilterChain", Filter.class)) + .build(); + mockMvc.perform(MockMvcRequestBuilders.get("/")) + .andExpect(MockMvcResultMatchers.header().string("Content-Security-Policy-Report-Only", + is("default-src 'self';"))) + .andExpect(MockMvcResultMatchers.header().doesNotExist("Content-Security-Policy")); + } + @Configuration @Import(TestWebConfiguration.class) @Order(Ordered.LOWEST_PRECEDENCE) diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 12fbb18348..c80d4c405a 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -431,6 +431,8 @@ content into your application; rather pick only the properties that you need. security.filter-order=0 # Security filter chain order. security.filter-dispatcher-types=ASYNC, FORWARD, INCLUDE, REQUEST # Security filter chain dispatcher types. security.headers.cache=true # Enable cache control HTTP headers. + security.headers.content-security-policy= # Value for content security policy header. + security.headers.content-security-policy-mode=default # Content security policy mode (default, report-only). security.headers.content-type=true # Enable "X-Content-Type-Options" header. security.headers.frame=true # Enable "X-Frame-Options" header. security.headers.hsts= # HTTP Strict Transport Security (HSTS) mode (none, domain, all).