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 3caa6e2d7e..b1d06ba226 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 @@ -729,7 +729,10 @@ public class HeadersConfigurer> * If false, will not specify the mode as blocked. In this instance, any content * will be attempted to be fixed. If true, the content will be replaced with "#". * @param enabled the new value + * @deprecated use + * {@link XXssConfig#headerValue(XXssProtectionHeaderWriter.HeaderValue)} instead */ + @Deprecated public XXssConfig block(boolean enabled) { this.writer.setBlock(enabled); return this; @@ -757,12 +760,49 @@ public class HeadersConfigurer> * X-XSS-Protection: 0 * * @param enabled the new value + * @deprecated use + * {@link XXssConfig#headerValue(XXssProtectionHeaderWriter.HeaderValue)} instead */ + @Deprecated public XXssConfig xssProtectionEnabled(boolean enabled) { this.writer.setEnabled(enabled); return this; } + /** + * Sets the value of the X-XSS-PROTECTION header. OWASP recommends using + * {@link XXssProtectionHeaderWriter.HeaderValue#DISABLED}. + * + * If {@link XXssProtectionHeaderWriter.HeaderValue#DISABLED}, will specify that + * X-XSS-Protection is disabled. For example: + * + *
+		 * X-XSS-Protection: 0
+		 * 
+ * + * If {@link XXssProtectionHeaderWriter.HeaderValue#ENABLED}, will contain a value + * of 1, but will not specify the mode as blocked. In this instance, any content + * will be attempted to be fixed. For example: + * + *
+		 * X-XSS-Protection: 1
+		 * 
+ * + * If {@link XXssProtectionHeaderWriter.HeaderValue#ENABLED_MODE_BLOCK}, will + * contain a value of 1 and will specify mode as blocked. The content will be + * replaced with "#". For example: + * + *
+		 * X-XSS-Protection: 1 ; mode=block
+		 * 
+ * @param headerValue the new header value + * @since 5.8 + */ + public XXssConfig headerValue(XXssProtectionHeaderWriter.HeaderValue headerValue) { + this.writer.setHeaderValue(headerValue); + return this; + } + /** * Disables X-XSS-Protection header (does not include it) * @return the {@link HeadersConfigurer} for additional configuration diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 6610e34c4c..3d0d985117 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -2859,6 +2859,18 @@ public class ServerHttpSecurity { return HeaderSpec.this; } + /** + * Sets the value of x-xss-protection header. OWASP recommends using + * {@link XXssProtectionServerHttpHeadersWriter.HeaderValue#DISABLED}. + * @param headerValue the headerValue + * @return the {@link HeaderSpec} to continue configuring + * @since 5.8 + */ + public HeaderSpec headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue headerValue) { + HeaderSpec.this.xss.setHeaderValue(headerValue); + return HeaderSpec.this; + } + } /** diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt index 9166acf4fe..e64897a4ee 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -16,16 +16,21 @@ package org.springframework.security.config.web.server +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter.HeaderValue + /** * A Kotlin DSL to configure the [ServerHttpSecurity] XSS protection header using * idiomatic Kotlin code. * + * @property headerValue the value of the X-XSS-Protection header. OWASP recommends [HeaderValue.DISABLED]. + * * @author Eleftheria Stein * @since 5.4 */ @ServerSecurityMarker class ServerXssProtectionDsl { private var disabled = false + var headerValue: HeaderValue? = null /** * Disables cache control response headers @@ -36,6 +41,7 @@ class ServerXssProtectionDsl { internal fun get(): (ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit { return { xss -> + headerValue?.also { xss.headerValue(headerValue) } if (disabled) { xss.disable() } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt index a48a30af10..a2fae4dbd5 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -18,6 +18,7 @@ package org.springframework.security.config.web.servlet.headers import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue /** * A Kotlin DSL to configure the [HttpSecurity] XSS protection header using @@ -28,11 +29,15 @@ import org.springframework.security.config.annotation.web.configurers.HeadersCon * @property block whether to specify the mode as blocked * @property xssProtectionEnabled if true, the header value will contain a value of 1. * If false, will explicitly disable specify that X-XSS-Protection is disabled. + * @property headerValue the value of the X-XSS-Protection header. OWASP recommends [HeaderValue.DISABLED]. */ @HeadersSecurityMarker class XssProtectionConfigDsl { + @Deprecated("use headerValue instead") var block: Boolean? = null + @Deprecated("use headerValue instead") var xssProtectionEnabled: Boolean? = null + var headerValue: HeaderValue? = null private var disabled = false @@ -47,6 +52,7 @@ class XssProtectionConfigDsl { return { xssProtection -> block?.also { xssProtection.block(block!!) } xssProtectionEnabled?.also { xssProtection.xssProtectionEnabled(xssProtectionEnabled!!) } + headerValue?.also { xssProtection.headerValue(headerValue) } if (disabled) { xssProtection.disable() 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 2b2f2a0b55..fb3d994a5d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -37,6 +37,7 @@ import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicy import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -58,6 +59,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Vedran Pavic * @author Eleftheria Stein * @author Marcus Da Coregio + * @author Daniel Garnier-Moiroux */ @ExtendWith(SpringTestContextExtension.class) public class HeadersConfigurerTests { @@ -171,6 +173,15 @@ public class HeadersConfigurerTests { assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); } + @Test + public void getWhenHeaderDefaultsDisabledAndXssProtectionConfiguredValueDisabledThenOnlyXssProtectionHeaderInResponse() + throws Exception { + this.spring.register(XssProtectionValueDisabledConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "0")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); + } + @Test public void getWhenOnlyXssProtectionConfiguredInLambdaThenOnlyXssProtectionHeaderInResponse() throws Exception { this.spring.register(XssProtectionInLambdaConfig.class).autowire(); @@ -179,6 +190,15 @@ public class HeadersConfigurerTests { assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); } + @Test + public void getWhenHeaderDefaultsDisabledAndXssProtectionConfiguredValueDisabledInLambdaThenOnlyXssProtectionHeaderInResponse() + throws Exception { + this.spring.register(XssProtectionValueDisabledInLambdaConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "0")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); + } + @Test public void getWhenFrameOptionsSameOriginConfiguredThenFrameOptionsHeaderHasValueSameOrigin() throws Exception { this.spring.register(HeadersCustomSameOriginConfig.class).autowire(); @@ -679,6 +699,22 @@ public class HeadersConfigurerTests { } + @EnableWebSecurity + static class XssProtectionValueDisabledConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers() + .defaultsDisabled() + .xssProtection() + .headerValue(XXssProtectionHeaderWriter.HeaderValue.DISABLED); + // @formatter:on + } + + } + @EnableWebSecurity static class XssProtectionInLambdaConfig extends WebSecurityConfigurerAdapter { @@ -696,6 +732,25 @@ public class HeadersConfigurerTests { } + @EnableWebSecurity + static class XssProtectionValueDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers((headers) -> + headers + .defaultsDisabled() + .xssProtection((xXssConfig) -> + xXssConfig.headerValue(XXssProtectionHeaderWriter.HeaderValue.DISABLED) + ) + ); + // @formatter:on + } + + } + @EnableWebSecurity static class HeadersCustomSameOriginConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.java index 30ae278b38..76e2088eb1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -31,6 +31,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.web.header.writers.StaticHeadersWriter; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; import org.springframework.security.web.header.writers.frameoptions.StaticAllowFromStrategy; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; import org.springframework.security.web.util.matcher.AnyRequestMatcher; @@ -273,8 +274,7 @@ public class NamespaceHttpHeadersTests { // xss-protection@enabled and xss-protection@block .defaultsDisabled() .xssProtection() - .xssProtectionEnabled(true) - .block(false); + .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index f4b85f45ba..001b965d96 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -296,6 +296,51 @@ public class HeaderSpecTests { assertHeaders(); } + @Test + public void headersWhenXssProtectionValueDisabledThenXssProtectionWritten() { + this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "0"); + // @formatter:off + this.http.headers() + .xssProtection() + .headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.DISABLED); + // @formatter:on + assertHeaders(); + } + + @Test + public void headersWhenXssProtectionValueEnabledThenXssProtectionWritten() { + this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1"); + // @formatter:off + this.http.headers() + .xssProtection() + .headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.ENABLED); + // @formatter:on + assertHeaders(); + } + + @Test + public void headersWhenXssProtectionValueEnabledModeBlockThenXssProtectionWritten() { + this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block"); + // @formatter:off + this.http.headers() + .xssProtection() + .headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.ENABLED_MODE_BLOCK); + // @formatter:on + assertHeaders(); + } + + @Test + public void headersWhenXssProtectionValueDisabledInLambdaThenXssProtectionWritten() { + this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "0"); + // @formatter:off + this.http.headers() + .xssProtection((xssProtection) -> + xssProtection.headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.DISABLED) + ); + // @formatter:on + assertHeaders(); + } + @Test public void headersWhenFeaturePolicyEnabledThenFeaturePolicyWritten() { String policyDirectives = "Feature-Policy"; diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt index 5816fb6aea..7e0799980d 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -96,4 +96,29 @@ class ServerXssProtectionDslTests { } } } + + @Test + fun `request when xss protection value disabled then xss header in response`() { + this.spring.register(XssValueDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "0") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class XssValueDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + xssProtection { + headerValue = XXssProtectionServerHttpHeadersWriter.HeaderValue.DISABLED + } + } + } + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt index 8b10f28cce..e8ac2e07a9 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -19,12 +19,16 @@ package org.springframework.security.config.web.servlet.headers import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.web.servlet.invoke import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get @@ -138,4 +142,32 @@ class XssProtectionConfigDslTests { } } } + + @Test + fun `headers when XSS protection header value disabled then X-XSS-Protection header is 0`() { + this.spring.register(XssProtectionHeaderValueDisabledFunctionConfig::class.java).autowire() + + this.mockMvc.get("/") { + secure = true + }.andExpect { + header { string(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "0") } + } + } + + @Configuration + @EnableWebSecurity + open class XssProtectionHeaderValueDisabledFunctionConfig () { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + headers { + defaultsDisabled = true + xssProtection { + headerValue = XXssProtectionHeaderWriter.HeaderValue.DISABLED + } + } + } + return http.build() + } + } } diff --git a/web/src/main/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriter.java index a5a72d1384..59111d40d4 100644 --- a/web/src/main/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriter.java +++ b/web/src/main/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; /** * Renders the * @param enabled the new value + * @deprecated use {@link XXssProtectionHeaderWriter#setHeaderValue(HeaderValue)} + * instead */ + @Deprecated public void setEnabled(boolean enabled) { if (!enabled) { - setBlock(false); + this.headerValue = HeaderValue.DISABLED; + } + else if (this.headerValue == HeaderValue.DISABLED) { + this.headerValue = HeaderValue.ENABLED; } - this.enabled = enabled; - updateHeaderValue(); } /** * If false, will not specify the mode as blocked. In this instance, any content will * be attempted to be fixed. If true, the content will be replaced with "#". * @param block the new value + * @deprecated use {@link XXssProtectionHeaderWriter#setHeaderValue(HeaderValue)} + * instead */ + @Deprecated public void setBlock(boolean block) { - if (!this.enabled && block) { + if (this.headerValue == HeaderValue.DISABLED && block) { throw new IllegalArgumentException("Cannot set block to true with enabled false"); } - this.block = block; - updateHeaderValue(); + this.headerValue = block ? HeaderValue.ENABLED_MODE_BLOCK : HeaderValue.ENABLED; } - private void updateHeaderValue() { - if (!this.enabled) { - this.headerValue = "0"; - return; + /** + * Sets the value of the X-XSS-PROTECTION header. + *

+ * If {@link HeaderValue#DISABLED}, will specify that X-XSS-Protection is disabled. + * For example: + * + *

+	 * X-XSS-Protection: 0
+	 * 
+ *

+ * If {@link HeaderValue#ENABLED}, will contain a value of 1, but will not specify the + * mode as blocked. In this instance, any content will be attempted to be fixed. For + * example: + * + *

+	 * X-XSS-Protection: 1
+	 * 
+ *

+ * If {@link HeaderValue#ENABLED_MODE_BLOCK}, will contain a value of 1 and will + * specify mode as blocked. The content will be replaced with "#". For example: + * + *

+	 * X-XSS-Protection: 1 ; mode=block
+	 * 
+ * @param headerValue the new header value + * @throws IllegalArgumentException when headerValue is null + * @since 5.8 + */ + public void setHeaderValue(HeaderValue headerValue) { + Assert.notNull(headerValue, "headerValue cannot be null"); + this.headerValue = headerValue; + } + + /** + * The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block" + * + * @author Daniel Garnier-Moiroux + * @since 5.8 + */ + public enum HeaderValue { + + DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1; mode=block"); + + private final String value; + + HeaderValue(String value) { + this.value = value; } - this.headerValue = "1"; - if (this.block) { - this.headerValue += "; mode=block"; + + @Override + public String toString() { + return this.value; } + } @Override diff --git a/web/src/main/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriter.java index 2437ed3042..7caa214358 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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. @@ -26,24 +26,22 @@ import org.springframework.web.server.ServerWebExchange; * Add the x-xss-protection header. * * @author Rob Winch + * @author Daniel Garnier-Moiroux * @since 5.0 */ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersWriter { public static final String X_XSS_PROTECTION = "X-XSS-Protection"; - private boolean enabled; - - private boolean block; - private ServerHttpHeadersWriter delegate; + private HeaderValue headerValue; + /** * Creates a new instance */ public XXssProtectionServerHttpHeadersWriter() { - this.enabled = true; - this.block = true; + this.headerValue = HeaderValue.ENABLED_MODE_BLOCK; updateDelegate(); } @@ -73,12 +71,17 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW * X-XSS-Protection: 0 * * @param enabled the new value + * @deprecated use + * {@link XXssProtectionServerHttpHeadersWriter#setHeaderValue(HeaderValue)} instead */ + @Deprecated public void setEnabled(boolean enabled) { if (!enabled) { - setBlock(false); + this.headerValue = HeaderValue.DISABLED; + } + else if (this.headerValue == HeaderValue.DISABLED) { + this.headerValue = HeaderValue.ENABLED; } - this.enabled = enabled; updateDelegate(); } @@ -86,27 +89,78 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW * If false, will not specify the mode as blocked. In this instance, any content will * be attempted to be fixed. If true, the content will be replaced with "#". * @param block the new value + * @deprecated use + * {@link XXssProtectionServerHttpHeadersWriter#setHeaderValue(HeaderValue)} instead */ + @Deprecated public void setBlock(boolean block) { - Assert.isTrue(this.enabled || !block, "Cannot set block to true with enabled false"); - this.block = block; + Assert.isTrue(this.headerValue != HeaderValue.DISABLED || !block, + "Cannot set block to true with enabled false"); + this.headerValue = block ? HeaderValue.ENABLED_MODE_BLOCK : HeaderValue.ENABLED; updateDelegate(); } + /** + * Sets the value of the X-XSS-PROTECTION header. + *

+ * If {@link HeaderValue#DISABLED}, will specify that X-XSS-Protection is disabled. + * For example: + * + *

+	 * X-XSS-Protection: 0
+	 * 
+ *

+ * If {@link HeaderValue#ENABLED}, will contain a value of 1, but will not specify the + * mode as blocked. In this instance, any content will be attempted to be fixed. For + * example: + * + *

+	 * X-XSS-Protection: 1
+	 * 
+ *

+ * If {@link HeaderValue#ENABLED_MODE_BLOCK}, will contain a value of 1 and will + * specify mode as blocked. The content will be replaced with "#". For example: + * + *

+	 * X-XSS-Protection: 1 ; mode=block
+	 * 
+ * @param headerValue the new headerValue + * @throws IllegalArgumentException if headerValue is null + * @since 5.8 + */ + public void setHeaderValue(HeaderValue headerValue) { + Assert.notNull(headerValue, "headerValue cannot be null"); + this.headerValue = headerValue; + updateDelegate(); + } + + /** + * The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block" + * + * @author Daniel Garnier-Moiroux + * @since 5.8 + */ + public enum HeaderValue { + + DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1 ; mode=block"); + + private final String value; + + HeaderValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + } + private void updateDelegate() { Builder builder = StaticServerHttpHeadersWriter.builder(); - builder.header(X_XSS_PROTECTION, createHeaderValue()); + builder.header(X_XSS_PROTECTION, this.headerValue.toString()); this.delegate = builder.build(); } - private String createHeaderValue() { - if (!this.enabled) { - return "0"; - } - if (!this.block) { - return "1"; - } - return "1 ; mode=block"; - } - } diff --git a/web/src/test/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriterTests.java index d9c51ba770..9245ae0135 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/XXssProtectionHeaderWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -94,4 +94,34 @@ public class XXssProtectionHeaderWriterTests { assertThat(this.response.getHeader(XSS_PROTECTION_HEADER)).isSameAs(value); } + @Test + void writeHeaderWhenDisabled() { + this.writer.setHeaderValue(XXssProtectionHeaderWriter.HeaderValue.DISABLED); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeaderValues("X-XSS-Protection")).containsOnly("0"); + } + + @Test + void writeHeaderWhenEnabled() { + this.writer.setHeaderValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeaderValues("X-XSS-Protection")).containsOnly("1"); + } + + @Test + void writeHeaderWhenEnabledModeBlock() { + this.writer.setHeaderValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeaderValues("X-XSS-Protection")).containsOnly("1; mode=block"); + } + + @Test + public void setHeaderValueNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setHeaderValue(null)) + .withMessage("headerValue cannot be null"); + } + } diff --git a/web/src/test/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriterTests.java index ca789c5a2e..7445810a9f 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/XXssProtectionServerHttpHeadersWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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. @@ -24,6 +24,7 @@ import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * @author Rob Winch @@ -37,6 +38,12 @@ public class XXssProtectionServerHttpHeadersWriterTests { XXssProtectionServerHttpHeadersWriter writer = new XXssProtectionServerHttpHeadersWriter(); + @Test + void setHeaderValueNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setHeaderValue(null)) + .withMessage("headerValue cannot be null"); + } + @Test public void writeHeadersWhenNoHeadersThenWriteHeaders() { this.writer.writeHttpHeaders(this.exchange); @@ -70,4 +77,29 @@ public class XXssProtectionServerHttpHeadersWriterTests { assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)).containsOnly(headerValue); } + @Test + void writeHeadersWhenDisabledThenWriteHeaders() { + this.writer.setHeaderValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.DISABLED); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers).hasSize(1); + assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)).containsOnly("0"); + } + + @Test + void writeHeadersWhenEnabledThenWriteHeaders() { + this.writer.setHeaderValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.ENABLED); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers).hasSize(1); + assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)).containsOnly("1"); + } + + @Test + void writeHeadersWhenEnabledModeBlockThenWriteHeaders() { + this.writer.setHeaderValue(XXssProtectionServerHttpHeadersWriter.HeaderValue.ENABLED_MODE_BLOCK); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers).hasSize(1); + assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION)) + .containsOnly("1 ; mode=block"); + } + }