From d7bbea63b77d55db0db34f9f488ac6e51debdcfa Mon Sep 17 00:00:00 2001 From: Christoffer Sawicki Date: Sat, 12 Nov 2016 20:36:54 +0100 Subject: [PATCH 1/2] 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). From 238c22cd737f62b39bf98dde5752a517182bfaa8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 16 Nov 2016 14:27:33 -0800 Subject: [PATCH 2/2] Polish content security policy contribution See gh-7373 See gh-7373 --- .../security/SecurityProperties.java | 11 +++++++---- .../SpringBootWebSecurityConfiguration.java | 10 ++++++---- ...SpringBootWebSecurityConfigurationTests.java | 17 ++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) 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 97d2ad4795..e157173a7f 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 @@ -175,15 +175,17 @@ public class SecurityProperties implements SecurityPrerequisite { } public enum ContentSecurityPolicyMode { + /** - * Use the {@code Content-Security-Policy} header. + * Use the 'Content-Security-Policy' header. */ DEFAULT, /** - * Use the {@code Content-Security-Policy-Report-Only} header. + * Use the 'Content-Security-Policy-Report-Only' header. */ REPORT_ONLY + } /** @@ -212,7 +214,7 @@ public class SecurityProperties implements SecurityPrerequisite { private String contentSecurityPolicy; /** - * Whether to use the "Content-Security-Policy" or "Content-Security-Policy-Report-Only" header. + * Security policy mode. */ private ContentSecurityPolicyMode contentSecurityPolicyMode = ContentSecurityPolicyMode.DEFAULT; @@ -265,7 +267,8 @@ public class SecurityProperties implements SecurityPrerequisite { return this.contentSecurityPolicyMode; } - public void setContentSecurityPolicyMode(ContentSecurityPolicyMode contentSecurityPolicyMode) { + public void setContentSecurityPolicyMode( + ContentSecurityPolicyMode contentSecurityPolicyMode) { this.contentSecurityPolicyMode = contentSecurityPolicyMode; } 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 daa5e3569a..0197ca2ee7 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 @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; +import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers.ContentSecurityPolicyMode; import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -109,12 +110,13 @@ public class SpringBootWebSecurityConfiguration { configurer.contentTypeOptions().disable(); } if (StringUtils.hasText(headers.getContentSecurityPolicy())) { - if (headers.getContentSecurityPolicyMode() == Headers.ContentSecurityPolicyMode.DEFAULT) { - configurer.contentSecurityPolicy(headers.getContentSecurityPolicy()); + String policyDirectives = headers.getContentSecurityPolicy(); + ContentSecurityPolicyMode mode = headers.getContentSecurityPolicyMode(); + if (mode == ContentSecurityPolicyMode.DEFAULT) { + configurer.contentSecurityPolicy(policyDirectives); } else { - assert headers.getContentSecurityPolicyMode() == Headers.ContentSecurityPolicyMode.REPORT_ONLY; - configurer.contentSecurityPolicy(headers.getContentSecurityPolicy()).reportOnly(); + configurer.contentSecurityPolicy(policyDirectives).reportOnly(); } } if (!headers.isXss()) { 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 23af97a970..e20cbb74d9 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 @@ -214,7 +214,8 @@ public class SpringBootWebSecurityConfigurationTests { is(notNullValue()))) .andExpect(MockMvcResultMatchers.header().string("X-Frame-Options", is(notNullValue()))) - .andExpect(MockMvcResultMatchers.header().doesNotExist("Content-Security-Policy")); + .andExpect(MockMvcResultMatchers.header() + .doesNotExist("Content-Security-Policy")); } @Test @@ -250,9 +251,10 @@ public class SpringBootWebSecurityConfigurationTests { .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")); + .andExpect(MockMvcResultMatchers.header() + .string("Content-Security-Policy", is("default-src 'self';"))) + .andExpect(MockMvcResultMatchers.header() + .doesNotExist("Content-Security-Policy-Report-Only")); } @Test @@ -266,9 +268,10 @@ public class SpringBootWebSecurityConfigurationTests { .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")); + .andExpect(MockMvcResultMatchers.header().string( + "Content-Security-Policy-Report-Only", is("default-src 'self';"))) + .andExpect(MockMvcResultMatchers.header() + .doesNotExist("Content-Security-Policy")); } @Configuration