From 93250013e48fd249e219bc14d6f30beb5bf91bf4 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 27 Sep 2022 14:56:00 +0200 Subject: [PATCH] Make X-Xss-Protection configurable through ServerHttpSecurity OWASP recommends using "X-Xss-Protection: 0". The default is currently "X-Xss-Protection: 1; mode=block". In 6.0, the default will be "0". This commits adds the ability to configure the xssProtection header value in ServerHttpSecurity. This commit deprecates the use of "enabled" and "block" booleans to configure XSS protection, as the state "!enabled + block" is invalid. This impacts HttpSecurity. Issue gh-9631 --- .../web/configurers/HeadersConfigurer.java | 40 ++++++++ .../config/web/server/ServerHttpSecurity.java | 12 +++ .../web/server/ServerXssProtectionDsl.kt | 8 +- .../servlet/headers/XssProtectionConfigDsl.kt | 8 +- .../configurers/HeadersConfigurerTests.java | 57 ++++++++++- .../NamespaceHttpHeadersTests.java | 6 +- .../config/web/server/HeaderSpecTests.java | 47 ++++++++- .../web/server/ServerXssProtectionDslTests.kt | 27 ++++- .../headers/XssProtectionConfigDslTests.kt | 34 ++++++- .../writers/XXssProtectionHeaderWriter.java | 92 ++++++++++++----- ...XXssProtectionServerHttpHeadersWriter.java | 98 ++++++++++++++----- .../XXssProtectionHeaderWriterTests.java | 32 +++++- ...rotectionServerHttpHeadersWriterTests.java | 34 ++++++- 13 files changed, 439 insertions(+), 56 deletions(-) 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"); + } + }