From 758397f1020a107294f1ea08a7700a03e437fb74 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 25 Jun 2019 15:27:34 -0400 Subject: [PATCH] Allow configuration of headers through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 97 +++++ .../web/configurers/HeadersConfigurer.java | 188 +++++++++- .../configurers/HeadersConfigurerTests.java | 338 ++++++++++++++++++ .../ContentSecurityPolicyHeaderWriter.java | 10 + ...ontentSecurityPolicyHeaderWriterTests.java | 19 + 5 files changed, 651 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index bad4a7fbb4..75e818caf9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -338,6 +338,103 @@ public final class HttpSecurity extends return getOrApply(new HeadersConfigurer<>()); } + /** + * Adds the Security headers to the response. This is activated by default when using + * {@link WebSecurityConfigurerAdapter}'s default constructor. + * + *

Example Configurations

+ * + * Accepting the default provided by {@link WebSecurityConfigurerAdapter} or only invoking + * {@link #headers()} without invoking additional methods on it, is the equivalent of: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *					.contentTypeOptions(withDefaults())
+	 *					.xssProtection(withDefaults())
+	 *					.cacheControl(withDefaults())
+	 *					.httpStrictTransportSecurity(withDefaults())
+	 *					.frameOptions(withDefaults()
+	 *			);
+	 *	}
+	 * }
+	 * 
+ * + * You can disable the headers using the following: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.headers(headers -> headers.disable());
+	 *	}
+	 * }
+	 * 
+ * + * You can enable only a few of the headers by first invoking + * {@link HeadersConfigurer#defaultsDisabled()} + * and then invoking the appropriate methods on the {@link #headers()} result. + * For example, the following will enable {@link HeadersConfigurer#cacheControl()} and + * {@link HeadersConfigurer#frameOptions()} only. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *			 		.defaultsDisabled()
+	 *			 		.cacheControl(withDefaults())
+	 *			 		.frameOptions(withDefaults())
+	 *			);
+	 * 	}
+	 * }
+	 * 
+ * + * You can also choose to keep the defaults but explicitly disable a subset of headers. + * For example, the following will enable all the default headers except + * {@link HeadersConfigurer#frameOptions()}. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 *  protected void configure(HttpSecurity http) throws Exception {
+	 *  	http
+	 *  		.headers(headers ->
+	 *  			headers
+	 *  				.frameOptions(frameOptions -> frameOptions.disable())
+	 *  		);
+	 * }
+	 * 
+ * + * @param headersCustomizer the {@link Customizer} to provide more options for + * the {@link HeadersConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity headers(Customizer> headersCustomizer) throws Exception { + headersCustomizer.customize(getOrApply(new HeadersConfigurer<>())); + return HttpSecurity.this; + } + /** * Adds a {@link CorsFilter} to be used. If a bean by the name of corsFilter is * provided, that {@link CorsFilter} is used. Else if corsConfigurationSource is diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 1f38253d40..77c840d5a0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -30,6 +31,8 @@ import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.*; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; +import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; 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; @@ -121,6 +124,26 @@ public class HeadersConfigurer> extends return contentTypeOptions.enable(); } + /** + * Configures the {@link XContentTypeOptionsHeaderWriter} which inserts the X-Content-Type-Options: + * + *
+	 * X-Content-Type-Options: nosniff
+	 * 
+ * + * @param contentTypeOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link ContentTypeOptionsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer contentTypeOptions(Customizer contentTypeOptionsCustomizer) + throws Exception { + contentTypeOptionsCustomizer.customize(contentTypeOptions.enable()); + return HeadersConfigurer.this; + } + public final class ContentTypeOptionsConfig { private XContentTypeOptionsHeaderWriter writer; @@ -174,6 +197,25 @@ public class HeadersConfigurer> extends return xssProtection.enable(); } + /** + * Note this is not comprehensive XSS protection! + * + *

+ * Allows customizing the {@link XXssProtectionHeaderWriter} which adds the X-XSS-Protection header + *

+ * + * @param xssCustomizer the {@link Customizer} to provide more options for + * the {@link XXssConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer xssProtection(Customizer xssCustomizer) throws Exception { + xssCustomizer.customize(xssProtection.enable()); + return HeadersConfigurer.this; + } + public final class XXssConfig { private XXssProtectionHeaderWriter writer; @@ -268,6 +310,26 @@ public class HeadersConfigurer> extends return cacheControl.enable(); } + /** + * Allows customizing the {@link CacheControlHeadersWriter}. Specifically it adds the + * following headers: + *
    + *
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • + *
  • Pragma: no-cache
  • + *
  • Expires: 0
  • + *
+ * + * @param cacheControlCustomizer the {@link Customizer} to provide more options for + * the {@link CacheControlConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer cacheControl(Customizer cacheControlCustomizer) throws Exception { + cacheControlCustomizer.customize(cacheControl.enable()); + return HeadersConfigurer.this; + } + + public final class CacheControlConfig { private CacheControlHeadersWriter writer; @@ -319,6 +381,21 @@ public class HeadersConfigurer> extends return hsts.enable(); } + /** + * Allows customizing the {@link HstsHeaderWriter} which provides support for HTTP Strict Transport Security + * (HSTS). + * + * @param hstsCustomizer the {@link Customizer} to provide more options for + * the {@link HstsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer httpStrictTransportSecurity(Customizer hstsCustomizer) throws Exception { + hstsCustomizer.customize(hsts.enable()); + return HeadersConfigurer.this; + } + public final class HstsConfig { private HstsHeaderWriter writer; @@ -440,6 +517,19 @@ public class HeadersConfigurer> extends return frameOptions.enable(); } + /** + * Allows customizing the {@link XFrameOptionsHeaderWriter}. + * + * @param frameOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link FrameOptionsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer frameOptions(Customizer frameOptionsCustomizer) throws Exception { + frameOptionsCustomizer.customize(frameOptions.enable()); + return HeadersConfigurer.this; + } + public final class FrameOptionsConfig { private XFrameOptionsHeaderWriter writer; @@ -516,6 +606,20 @@ public class HeadersConfigurer> extends return hpkp.enable(); } + /** + * Allows customizing the {@link HpkpHeaderWriter} which provides support for HTTP Public Key Pinning (HPKP). + * + * @param hpkpCustomizer the {@link Customizer} to provide more options for + * the {@link HpkpConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer httpPublicKeyPinning(Customizer hpkpCustomizer) throws Exception { + hpkpCustomizer.customize(hpkp.enable()); + return HeadersConfigurer.this; + } + public final class HpkpConfig { private HpkpHeaderWriter writer; @@ -713,12 +817,57 @@ public class HeadersConfigurer> extends return contentSecurityPolicy; } + /** + *

+ * Allows configuration for Content Security Policy (CSP) Level 2. + *

+ * + *

+ * Calling this method automatically enables (includes) the Content-Security-Policy header in the response + * using the supplied security policy directive(s). + *

+ * + *

+ * Configuration is provided to the {@link ContentSecurityPolicyHeaderWriter} which supports the writing + * of the two headers as detailed in the W3C Candidate Recommendation: + *

+ *
    + *
  • Content-Security-Policy
  • + *
  • Content-Security-Policy-Report-Only
  • + *
+ * + * @see ContentSecurityPolicyHeaderWriter + * @param contentSecurityCustomizer the {@link Customizer} to provide more options for + * the {@link ContentSecurityPolicyConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer contentSecurityPolicy(Customizer contentSecurityCustomizer) + throws Exception { + this.contentSecurityPolicy.writer = new ContentSecurityPolicyHeaderWriter(); + contentSecurityCustomizer.customize(this.contentSecurityPolicy); + + return HeadersConfigurer.this; + } + public final class ContentSecurityPolicyConfig { private ContentSecurityPolicyHeaderWriter writer; private ContentSecurityPolicyConfig() { } + /** + * Sets the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @return the {@link ContentSecurityPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policyDirectives is null or empty + */ + public ContentSecurityPolicyConfig policyDirectives(String policyDirectives) { + this.writer.setPolicyDirectives(policyDirectives); + return this; + } + /** * Enables (includes) the Content-Security-Policy-Report-Only header in the response. * @@ -860,6 +1009,31 @@ public class HeadersConfigurer> extends return this.referrerPolicy; } + /** + *

+ * Allows configuration for Referrer Policy. + *

+ * + *

+ * Configuration is provided to the {@link ReferrerPolicyHeaderWriter} which support the writing + * of the header as detailed in the W3C Technical Report: + *

+ *
    + *
  • Referrer-Policy
  • + *
+ * + * @see ReferrerPolicyHeaderWriter + * @param referrerPolicyCustomizer the {@link Customizer} to provide more options for + * the {@link ReferrerPolicyConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer referrerPolicy(Customizer referrerPolicyCustomizer) throws Exception { + this.referrerPolicy.writer = new ReferrerPolicyHeaderWriter(); + referrerPolicyCustomizer.customize(this.referrerPolicy); + return HeadersConfigurer.this; + } + public final class ReferrerPolicyConfig { private ReferrerPolicyHeaderWriter writer; @@ -867,6 +1041,18 @@ public class HeadersConfigurer> extends private ReferrerPolicyConfig() { } + /** + * Sets the policy to be used in the response header. + * + * @param policy a referrer policy + * @return the {@link ReferrerPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policy is null + */ + public ReferrerPolicyConfig policy(ReferrerPolicy policy) { + this.writer.setPolicy(policy); + return this; + } + public HeadersConfigurer and() { return HeadersConfigurer.this; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java index f8ed9adcf2..0ca822a6fd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java @@ -36,6 +36,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -87,6 +88,36 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenHeadersConfiguredInLambdaThenDefaultHeadersInResponse() throws Exception { + this.spring.register(HeadersInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_CONTENT_TYPE_OPTIONS, "nosniff")) + .andExpect(header().string(HttpHeaders.X_FRAME_OPTIONS, XFrameOptionsMode.DENY.name())) + .andExpect(header().string(HttpHeaders.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")) + .andExpect(header().string(HttpHeaders.EXPIRES, "0")) + .andExpect(header().string(HttpHeaders.PRAGMA, "no-cache")) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "1; mode=block")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactlyInAnyOrder( + HttpHeaders.X_CONTENT_TYPE_OPTIONS, HttpHeaders.X_FRAME_OPTIONS, HttpHeaders.STRICT_TRANSPORT_SECURITY, + HttpHeaders.CACHE_CONTROL, HttpHeaders.EXPIRES, HttpHeaders.PRAGMA, HttpHeaders.X_XSS_PROTECTION); + } + + @EnableWebSecurity + static class HeadersInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(withDefaults()); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndContentTypeConfiguredThenOnlyContentTypeHeaderInResponse() throws Exception { @@ -112,6 +143,33 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenOnlyContentTypeConfiguredInLambdaThenOnlyContentTypeHeaderInResponse() + throws Exception { + this.spring.register(ContentTypeOptionsInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.X_CONTENT_TYPE_OPTIONS, "nosniff")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_CONTENT_TYPE_OPTIONS); + } + + @EnableWebSecurity + static class ContentTypeOptionsInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentTypeOptions(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndFrameOptionsConfiguredThenOnlyFrameOptionsHeaderInResponse() throws Exception { @@ -190,6 +248,36 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenOnlyCacheControlConfiguredInLambdaThenCacheControlAndExpiresAndPragmaHeadersInResponse() + throws Exception { + this.spring.register(CacheControlInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")) + .andExpect(header().string(HttpHeaders.EXPIRES, "0")) + .andExpect(header().string(HttpHeaders.PRAGMA, "no-cache")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactlyInAnyOrder(HttpHeaders.CACHE_CONTROL, + HttpHeaders.EXPIRES, HttpHeaders.PRAGMA); + } + + @EnableWebSecurity + static class CacheControlInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .cacheControl(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndXssProtectionConfiguredThenOnlyXssProtectionHeaderInResponse() throws Exception { @@ -215,6 +303,33 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenOnlyXssProtectionConfiguredInLambdaThenOnlyXssProtectionHeaderInResponse() + throws Exception { + this.spring.register(XssProtectionInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "1; mode=block")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); + } + + @EnableWebSecurity + static class XssProtectionInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .xssProtection(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenFrameOptionsSameOriginConfiguredThenFrameOptionsHeaderHasValueSameOrigin() throws Exception { this.spring.register(HeadersCustomSameOriginConfig.class).autowire(); @@ -237,6 +352,31 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenFrameOptionsSameOriginConfiguredInLambdaThenFrameOptionsHeaderHasValueSameOrigin() + throws Exception { + this.spring.register(HeadersCustomSameOriginInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_FRAME_OPTIONS, XFrameOptionsMode.SAMEORIGIN.name())) + .andReturn(); + } + + @EnableWebSecurity + static class HeadersCustomSameOriginInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .frameOptions(frameOptionsConfig -> frameOptionsConfig.sameOrigin()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndPublicHpkpWithNoPinThenNoHeadersInResponse() throws Exception { this.spring.register(HpkpConfigNoPins.class).autowire(); @@ -465,6 +605,38 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenHpkpWithReportUriInLambdaThenPublicKeyPinsReportOnlyHeaderWithReportUriInResponse() + throws Exception { + this.spring.register(HpkpWithReportUriInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.PUBLIC_KEY_PINS_REPORT_ONLY, + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; report-uri=\"https://example.net/pkp-report\"")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.PUBLIC_KEY_PINS_REPORT_ONLY); + } + + @EnableWebSecurity + static class HpkpWithReportUriInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .httpPublicKeyPinning(hpkp -> + hpkp + .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=") + .reportUri("https://example.net/pkp-report") + ) + ); + // @formatter:on + } + } + @Test public void getWhenContentSecurityPolicyConfiguredThenContentSecurityPolicyHeaderInResponse() throws Exception { this.spring.register(ContentSecurityPolicyDefaultConfig.class).autowire(); @@ -515,6 +687,38 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenContentSecurityPolicyWithReportOnlyInLambdaThenContentSecurityPolicyReportOnlyHeaderInResponse() + throws Exception { + this.spring.register(ContentSecurityPolicyReportOnlyInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY, + "default-src 'self'; script-src trustedscripts.example.com")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY); + } + + @EnableWebSecurity + static class ContentSecurityPolicyReportOnlyInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(csp -> + csp + .policyDirectives("default-src 'self'; script-src trustedscripts.example.com") + .reportOnly() + ) + ); + // @formatter:on + } + } + @Test public void configureWhenContentSecurityPolicyEmptyThenException() { assertThatThrownBy(() -> this.spring.register(ContentSecurityPolicyInvalidConfig.class).autowire()) @@ -536,6 +740,58 @@ public class HeadersConfigurerTests { } } + @Test + public void configureWhenContentSecurityPolicyEmptyInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(ContentSecurityPolicyInvalidInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class ContentSecurityPolicyInvalidInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(csp -> + csp.policyDirectives("") + ) + ); + // @formatter:on + } + } + + @Test + public void configureWhenContentSecurityPolicyNoPolicyDirectivesInLambdaThenDefaultHeaderValue() throws Exception { + this.spring.register(ContentSecurityPolicyNoDirectivesInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CONTENT_SECURITY_POLICY, + "default-src 'self'")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CONTENT_SECURITY_POLICY); + } + + @EnableWebSecurity + static class ContentSecurityPolicyNoDirectivesInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenReferrerPolicyConfiguredThenReferrerPolicyHeaderInResponse() throws Exception { this.spring.register(ReferrerPolicyDefaultConfig.class).autowire(); @@ -560,6 +816,32 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenReferrerPolicyInLambdaThenReferrerPolicyHeaderInResponse() throws Exception { + this.spring.register(ReferrerPolicyDefaultInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string("Referrer-Policy", ReferrerPolicy.NO_REFERRER.getPolicy())) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Referrer-Policy"); + } + + @EnableWebSecurity + static class ReferrerPolicyDefaultInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .referrerPolicy() + ); + // @formatter:on + } + } + @Test public void getWhenReferrerPolicyConfiguredWithCustomValueThenReferrerPolicyHeaderWithCustomValueInResponse() throws Exception { @@ -585,6 +867,34 @@ public class HeadersConfigurerTests { } } + @Test + public void getWhenReferrerPolicyConfiguredWithCustomValueInLambdaThenCustomValueInResponse() throws Exception { + this.spring.register(ReferrerPolicyCustomInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string("Referrer-Policy", ReferrerPolicy.SAME_ORIGIN.getPolicy())) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Referrer-Policy"); + } + + @EnableWebSecurity + static class ReferrerPolicyCustomInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .referrerPolicy(referrerPolicy -> + referrerPolicy.policy(ReferrerPolicy.SAME_ORIGIN) + ) + ); + // @formatter:on + } + } + @Test public void getWhenFeaturePolicyConfiguredThenFeaturePolicyHeaderInResponse() throws Exception { this.spring.register(FeaturePolicyConfig.class).autowire(); @@ -656,4 +966,32 @@ public class HeadersConfigurerTests { // @formatter:on } } + + @Test + public void getWhenHstsConfiguredWithPreloadInLambdaThenStrictTransportSecurityHeaderWithPreloadInResponse() + throws Exception { + this.spring.register(HstsWithPreloadInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.STRICT_TRANSPORT_SECURITY, + "max-age=31536000 ; includeSubDomains ; preload")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.STRICT_TRANSPORT_SECURITY); + } + + @EnableWebSecurity + static class HstsWithPreloadInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .httpStrictTransportSecurity(hstsConfig -> hstsConfig.preload(true)) + ); + // @formatter:on + } + } } diff --git a/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java index 12ad08e978..4ae86198f3 100644 --- a/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java +++ b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java @@ -81,10 +81,20 @@ public final class ContentSecurityPolicyHeaderWriter implements HeaderWriter { private static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER = "Content-Security-Policy-Report-Only"; + private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'"; + private String policyDirectives; private boolean reportOnly; + /** + * Creates a new instance. Default value: default-src 'self' + */ + public ContentSecurityPolicyHeaderWriter() { + setPolicyDirectives(DEFAULT_SRC_SELF_POLICY); + this.reportOnly = false; + } + /** * Creates a new instance * diff --git a/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java index 5ff062f434..2f3c7914e8 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java @@ -43,6 +43,15 @@ public class ContentSecurityPolicyHeaderWriterTests { writer = new ContentSecurityPolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); } + @Test + public void writeHeadersWhenNoPolicyDirectivesThenUsesDefault() { + ContentSecurityPolicyHeaderWriter noPolicyWriter = new ContentSecurityPolicyHeaderWriter(); + noPolicyWriter.writeHeaders(request, response); + + assertThat(response.getHeaderNames()).hasSize(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + @Test public void writeHeadersContentSecurityPolicyDefault() { writer.writeHeaders(request, response); @@ -64,6 +73,16 @@ public class ContentSecurityPolicyHeaderWriterTests { assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(policyDirectives); } + @Test + public void writeHeadersWhenNoPolicyDirectivesReportOnlyThenUsesDefault() { + ContentSecurityPolicyHeaderWriter noPolicyWriter = new ContentSecurityPolicyHeaderWriter(); + writer.setReportOnly(true); + noPolicyWriter.writeHeaders(request, response); + + assertThat(response.getHeaderNames()).hasSize(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + @Test public void writeHeadersContentSecurityPolicyReportOnlyDefault() { writer.setReportOnly(true);