From ccbc4f91ec64b93b4d13d3f894229305762a30ff Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 14 Mar 2025 09:33:45 +0100 Subject: [PATCH 1/2] 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 797910b8ea..73642504ba 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 392ec235ae..42241de134 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 ac9ed313b7..c7bd34d8be 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 00c504e7db..6bdd590c68 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 6b6b0b62be..d10bf99ed4 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 40597690d7..4bc7f552c9 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 dcf8721610..26cfa8e35e 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 49b1340069..4546b4b863 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(); From 636707482906c67ca7d59b811f7ca8db90298246 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Mar 2025 12:52:01 +0000 Subject: [PATCH 2/2] Polish "Add support for omitting SameSite attribute from session cookie" See gh-44714 --- .../WebSessionIdResolverAutoConfiguration.java | 2 +- .../session/SessionAutoConfigurationTests.java | 2 +- .../springframework/boot/web/server/Cookie.java | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 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 73642504ba..ae1da0b818 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. 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 42241de134..aa79e77374 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. 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 26cfa8e35e..17fdfed085 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -146,24 +146,25 @@ public class Cookie { public enum SameSite { /** - * The SameSite cookie attribute will be omitted when creating the cookie. + * SameSite attribute will be omitted when creating the cookie. */ OMITTED(null), /** - * Cookies are sent in both first-party and cross-origin requests. + * SameSite attribute will be set to None. Cookies are sent in both first-party + * and cross-origin requests. */ NONE("None"), /** - * Cookies are sent in a first-party context, also when following a link to the - * origin site. + * SameSite attribute will be set to Lax. Cookies are sent in a first-party + * context, also when following a link to the origin site. */ LAX("Lax"), /** - * Cookies are only sent in a first-party context (i.e. not when following a link - * to the origin site). + * SameSite attribute will be set to Strict. Cookies are only sent in a + * first-party context (i.e. not when following a link to the origin site). */ STRICT("Strict");