From ccbc4f91ec64b93b4d13d3f894229305762a30ff Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 14 Mar 2025 09:33:45 +0100 Subject: [PATCH] Add support for omitting SameSite attribute from session cookie See gh-44714 Signed-off-by: Filip Hrisafov --- ...WebSessionIdResolverAutoConfiguration.java | 7 +------ .../SessionAutoConfigurationTests.java | 10 +++++++++ .../WebFluxAutoConfigurationTests.java | 9 ++++++++ .../jetty/JettyServletWebServerFactory.java | 2 +- .../tomcat/TomcatServletWebServerFactory.java | 5 +++-- .../UndertowServletWebServerFactory.java | 5 ++++- .../boot/web/server/Cookie.java | 5 +++++ .../AbstractServletWebServerFactoryTests.java | 21 +++++++++++++++++-- 8 files changed, 52 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java index 797910b8ea3..73642504ba4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java @@ -77,12 +77,7 @@ public class WebSessionIdResolverAutoConfiguration { map.from(cookie::getSecure).to(builder::secure); map.from(cookie::getMaxAge).to(builder::maxAge); map.from(cookie::getPartitioned).to(builder::partitioned); - map.from(getSameSite(cookie)).to(builder::sameSite); - } - - private String getSameSite(Cookie properties) { - SameSite sameSite = properties.getSameSite(); - return (sameSite != null) ? sameSite.attributeValue() : null; + map.from(cookie::getSameSite).as(SameSite::attributeValue).to(builder::sameSite); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index 392ec235aea..42241de134b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -170,6 +170,16 @@ class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTest }); } + @Test + void sessionCookieSameSiteOmittedIsAppliedToAutoConfiguredCookieSerializer() { + this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) + .withPropertyValues("server.servlet.session.cookie.sameSite=omitted") + .run((context) -> { + DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class); + assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", null); + }); + } + @Test void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index ac9ed313b72..c7bd34d8be9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -676,6 +676,15 @@ class WebFluxAutoConfigurationTests { })); } + @Test + void sessionCookieOmittedConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("server.reactive.session.cookie.same-site:omitted") + .run(assertExchangeWithSession((exchange) -> { + List cookies = exchange.getResponse().getCookies().get("SESSION"); + assertThat(cookies).extracting(ResponseCookie::getSameSite).containsOnlyNulls(); + })); + } + @ParameterizedTest @ValueSource(classes = { ServerProperties.class, WebFluxProperties.class }) void propertiesAreNotEnabledInNonWebApplication(Class propertiesClass) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 00c504e7db3..6bdd590c688 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -284,7 +284,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor private void configureSession(WebAppContext context) { SessionHandler handler = context.getSessionHandler(); SameSite sessionSameSite = getSession().getCookie().getSameSite(); - if (sessionSameSite != null) { + if (sessionSameSite != null && sessionSameSite != SameSite.OMITTED) { handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name())); } Duration sessionTimeout = getSession().getTimeout(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 6b6b0b62be0..d10bf99ed4c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -998,11 +998,12 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto @Override public String generateHeader(Cookie cookie, HttpServletRequest request) { SameSite sameSite = getSameSite(cookie); - if (sameSite == null) { + String sameSiteValue = (sameSite != null) ? sameSite.attributeValue() : null; + if (sameSiteValue == null) { return super.generateHeader(cookie, request); } Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor(); - delegate.setSameSiteCookies(sameSite.attributeValue()); + delegate.setSameSiteCookies(sameSiteValue); return delegate.generateHeader(cookie, request); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 40597690d77..4bc7f552c94 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -635,7 +635,10 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac private void beforeCommit(HttpServerExchange exchange) { for (Cookie cookie : exchange.responseCookies()) { SameSite sameSite = getSameSite(asServletCookie(cookie)); - if (sameSite != null) { + if (sameSite == SameSite.OMITTED) { + cookie.setSameSite(false); + } + else if (sameSite != null) { cookie.setSameSiteMode(sameSite.attributeValue()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java index dcf8721610b..26cfa8e35e8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java @@ -145,6 +145,11 @@ public class Cookie { */ public enum SameSite { + /** + * The SameSite cookie attribute will be omitted when creating the cookie. + */ + OMITTED(null), + /** * Cookies are sent in both first-party and cross-origin requests. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 49b1340069b..4546b4b8631 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -881,7 +881,7 @@ public abstract class AbstractServletWebServerFactoryTests { } @ParameterizedTest - @EnumSource + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED") void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception { AbstractServletWebServerFactory factory = getFactory(); factory.getSession().getCookie().setSameSite(sameSite); @@ -896,7 +896,7 @@ public abstract class AbstractServletWebServerFactoryTests { } @ParameterizedTest - @EnumSource + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED") void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite) throws Exception { AbstractServletWebServerFactory factory = getFactory(); @@ -949,6 +949,23 @@ public abstract class AbstractServletWebServerFactoryTests { (header) -> assertThat(header).contains("test=test").contains("SameSite=Strict")); } + @Test + void cookieSameSiteSuppliersShouldNotAffectOmittedSameSite() throws IOException, URISyntaxException { + AbstractServletWebServerFactory factory = getFactory(); + factory.getSession().getCookie().setSameSite(SameSite.OMITTED); + factory.getSession().getCookie().setName("SESSIONCOOKIE"); + factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofStrict()); + factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/")); + assertThat(clientResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + List setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); + assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( + (header) -> assertThat(header).contains("SESSIONCOOKIE").doesNotContain("SameSite"), + (header) -> assertThat(header).contains("test=test").contains("SameSite=Strict")); + } + @Test protected void sslSessionTracking() { AbstractServletWebServerFactory factory = getFactory();