浏览代码

Allow configuration of headers through nested builder

Issue: gh-5557
Eleftheria Stein 6 年之前
父节点
当前提交
758397f102

+ 97 - 0
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.
+	 *
+	 * <h2>Example Configurations</h2>
+	 *
+	 * Accepting the default provided by {@link WebSecurityConfigurerAdapter} or only invoking
+	 * {@link #headers()} without invoking additional methods on it, is the equivalent of:
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	&#064;Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *					.contentTypeOptions(withDefaults())
+	 *					.xssProtection(withDefaults())
+	 *					.cacheControl(withDefaults())
+	 *					.httpStrictTransportSecurity(withDefaults())
+	 *					.frameOptions(withDefaults()
+	 *			);
+	 *	}
+	 * }
+	 * </pre>
+	 *
+	 * You can disable the headers using the following:
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	&#064;Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.headers(headers -> headers.disable());
+	 *	}
+	 * }
+	 * </pre>
+	 *
+	 * 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.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	&#064;Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *			 		.defaultsDisabled()
+	 *			 		.cacheControl(withDefaults())
+	 *			 		.frameOptions(withDefaults())
+	 *			);
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * 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()}.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 *  protected void configure(HttpSecurity http) throws Exception {
+	 *  	http
+	 *  		.headers(headers ->
+	 *  			headers
+	 *  				.frameOptions(frameOptions -> frameOptions.disable())
+	 *  		);
+	 * }
+	 * </pre>
+	 *
+	 * @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<HeadersConfigurer<HttpSecurity>> 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

+ 187 - 1
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<H extends HttpSecurityBuilder<H>> extends
 		return contentTypeOptions.enable();
 	}
 
+	/**
+	 * Configures the {@link XContentTypeOptionsHeaderWriter} which inserts the <a href=
+	 * "https://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx"
+	 * >X-Content-Type-Options</a>:
+	 *
+	 * <pre>
+	 * X-Content-Type-Options: nosniff
+	 * </pre>
+	 *
+	 * @param contentTypeOptionsCustomizer the {@link Customizer} to provide more options for
+	 * the {@link ContentTypeOptionsConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> contentTypeOptions(Customizer<ContentTypeOptionsConfig> contentTypeOptionsCustomizer)
+			throws Exception {
+		contentTypeOptionsCustomizer.customize(contentTypeOptions.enable());
+		return HeadersConfigurer.this;
+	}
+
 	public final class ContentTypeOptionsConfig {
 		private XContentTypeOptionsHeaderWriter writer;
 
@@ -174,6 +197,25 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
 		return xssProtection.enable();
 	}
 
+	/**
+	 * <strong>Note this is not comprehensive XSS protection!</strong>
+	 *
+	 * <p>
+	 * Allows customizing the {@link XXssProtectionHeaderWriter} which adds the <a href=
+	 * "https://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx"
+	 * >X-XSS-Protection header</a>
+	 * </p>
+	 *
+	 * @param xssCustomizer the {@link Customizer} to provide more options for
+	 * the {@link XXssConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> xssProtection(Customizer<XXssConfig> xssCustomizer) throws Exception {
+		xssCustomizer.customize(xssProtection.enable());
+		return HeadersConfigurer.this;
+	}
+
 	public final class XXssConfig {
 		private XXssProtectionHeaderWriter writer;
 
@@ -268,6 +310,26 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
 		return cacheControl.enable();
 	}
 
+	/**
+	 * Allows customizing the {@link CacheControlHeadersWriter}. Specifically it adds the
+	 * following headers:
+	 * <ul>
+	 * <li>Cache-Control: no-cache, no-store, max-age=0, must-revalidate</li>
+	 * <li>Pragma: no-cache</li>
+	 * <li>Expires: 0</li>
+	 * </ul>
+	 *
+	 * @param cacheControlCustomizer the {@link Customizer} to provide more options for
+	 * the {@link CacheControlConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> cacheControl(Customizer<CacheControlConfig> cacheControlCustomizer) throws Exception {
+		cacheControlCustomizer.customize(cacheControl.enable());
+		return HeadersConfigurer.this;
+	}
+
+
 	public final class CacheControlConfig {
 		private CacheControlHeadersWriter writer;
 
@@ -319,6 +381,21 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
 		return hsts.enable();
 	}
 
+	/**
+	 * Allows customizing the {@link HstsHeaderWriter} which provides support for <a
+	 * href="https://tools.ietf.org/html/rfc6797">HTTP Strict Transport Security
+	 * (HSTS)</a>.
+	 *
+	 * @param hstsCustomizer the {@link Customizer} to provide more options for
+	 * the {@link HstsConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> httpStrictTransportSecurity(Customizer<HstsConfig> hstsCustomizer) throws Exception {
+		hstsCustomizer.customize(hsts.enable());
+		return HeadersConfigurer.this;
+	}
+
 	public final class HstsConfig {
 		private HstsHeaderWriter writer;
 
@@ -440,6 +517,19 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> 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<H> frameOptions(Customizer<FrameOptionsConfig> frameOptionsCustomizer) throws Exception {
+		frameOptionsCustomizer.customize(frameOptions.enable());
+		return HeadersConfigurer.this;
+	}
+
 	public final class FrameOptionsConfig {
 		private XFrameOptionsHeaderWriter writer;
 
@@ -516,6 +606,20 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
 		return hpkp.enable();
 	}
 
+	/**
+	 * Allows customizing the {@link HpkpHeaderWriter} which provides support for <a
+	 * href="https://tools.ietf.org/html/rfc7469">HTTP Public Key Pinning (HPKP)</a>.
+	 *
+	 * @param hpkpCustomizer the {@link Customizer} to provide more options for
+	 * the {@link HpkpConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> httpPublicKeyPinning(Customizer<HpkpConfig> hpkpCustomizer) throws Exception {
+		hpkpCustomizer.customize(hpkp.enable());
+		return HeadersConfigurer.this;
+	}
+
 	public final class HpkpConfig {
 		private HpkpHeaderWriter writer;
 
@@ -713,12 +817,57 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
 		return contentSecurityPolicy;
 	}
 
+	/**
+	 * <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
+	 * @param contentSecurityCustomizer the {@link Customizer} to provide more options for
+	 * the {@link ContentSecurityPolicyConfig}
+	 * @return the {@link HeadersConfigurer} for additional customizations
+	 * @throws Exception
+	 */
+	public HeadersConfigurer<H> contentSecurityPolicy(Customizer<ContentSecurityPolicyConfig> 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<H extends HttpSecurityBuilder<H>> extends
 		return this.referrerPolicy;
 	}
 
+	/**
+	 * <p>
+	 * Allows configuration for <a href="https://www.w3.org/TR/referrer-policy/">Referrer Policy</a>.
+	 * </p>
+	 *
+	 * <p>
+	 * Configuration is provided to the {@link ReferrerPolicyHeaderWriter} which support the writing
+	 * of the header as detailed in the W3C Technical Report:
+	 * </p>
+	 * <ul>
+	 *  <li>Referrer-Policy</li>
+	 * </ul>
+	 *
+	 * @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<H> referrerPolicy(Customizer<ReferrerPolicyConfig> 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<H extends HttpSecurityBuilder<H>> 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<H> and() {
 			return HeadersConfigurer.this;
 		}

+ 338 - 0
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
+		}
+	}
 }

+ 10 - 0
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
 	 *

+ 19 - 0
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);