From 7767b5d284876810ab805aaf164e9524503a51d4 Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 22 Oct 2026 01:11:25 +0100 Subject: [PATCH 1/4] CXF-9072: Support the SameSite attribute in NewCookieHeaderProvider fromString treated any attribute it did not recognize, such as SameSite, as the cookie's name=value pair, so "foo=bar;SameSite=Lax" was parsed as a cookie named "SameSite". The cookie's own name=value pair now comes first and later unrecognized attributes are ignored. SameSite is parsed case-insensitively into NewCookie.SameSite, an unrecognized SameSite value is ignored, and toString writes the attribute so it survives a round trip through Response.getCookies(). --- .../jaxrs/impl/NewCookieHeaderProvider.java | 37 ++++++++++++- .../impl/NewCookieHeaderProviderTest.java | 53 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java index e4759978823..d22fd13e8a2 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java @@ -36,6 +36,7 @@ public class NewCookieHeaderProvider implements HeaderDelegate { private static final String SECURE = "Secure"; private static final String EXPIRES = "Expires"; private static final String HTTP_ONLY = "HttpOnly"; + private static final String SAME_SITE = "SameSite"; /** from RFC 2068, token special case characters */ @@ -59,6 +60,7 @@ public NewCookie fromString(String c) { Date expires = null; boolean httpOnly = false; int version = Cookie.DEFAULT_VERSION; + NewCookie.SameSite sameSite = null; String[] tokens = c.split(";"); for (String token : tokens) { @@ -94,7 +96,10 @@ public NewCookie fromString(String c) { httpOnly = true; } else if (paramName.equalsIgnoreCase(VERSION)) { version = Integer.parseInt(paramValue); - } else if (paramValue != null) { + } else if (paramName.equalsIgnoreCase(SAME_SITE)) { + sameSite = toSameSite(paramValue); + } else if (paramValue != null && name == null) { + // The cookie's own name=value pair comes first; unrecognized attributes after it are ignored name = paramName; value = paramValue; } @@ -104,7 +109,8 @@ public NewCookie fromString(String c) { throw new IllegalArgumentException("Set-Cookie is malformed : " + c); } - return new NewCookie(name, value, path, domain, version, comment, maxAge, expires, isSecure, httpOnly); + return new NewCookie(name, value, path, domain, version, comment, maxAge, expires, isSecure, httpOnly, + sameSite); } @Override @@ -137,11 +143,38 @@ public String toString(NewCookie value) { if (value.isHttpOnly()) { sb.append(';').append(HTTP_ONLY); } + if (value.getSameSite() != null) { + sb.append(';').append(SAME_SITE).append('=').append(toAttributeValue(value.getSameSite())); + } sb.append(';').append(VERSION).append('=').append(value.getVersion()); return sb.toString(); } + /** + * Matches the SameSite attribute value case-insensitively; an unrecognized value is ignored, + * as RFC 6265bis requires. + */ + private static NewCookie.SameSite toSameSite(String attributeValue) { + for (NewCookie.SameSite sameSite : NewCookie.SameSite.values()) { + if (sameSite.name().equalsIgnoreCase(attributeValue)) { + return sameSite; + } + } + return null; + } + + private static String toAttributeValue(NewCookie.SameSite sameSite) { + switch (sameSite) { + case STRICT: + return "Strict"; + case LAX: + return "Lax"; + default: + return "None"; + } + } + /** * Append the input value string to the given buffer, wrapping it with * quotes if need be. diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java index cc1ceed1a3d..8eb23e8b8c1 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class NewCookieHeaderProviderTest { @@ -151,4 +152,56 @@ public void testToStringWithPathSlalshOnly() { + "Path=/path;Secure;Version=1", c.toString()); } + @Test + public void testFromStringWithSameSite() { + NewCookie c = NewCookie.valueOf("foo=bar;SameSite=Lax;Path=/"); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertEquals("/", c.getPath()); + assertEquals(NewCookie.SameSite.LAX, c.getSameSite()); + } + + @Test + public void testFromStringWithSameSiteIsCaseInsensitive() { + NewCookie c = NewCookie.valueOf("foo=bar; samesite=none; Secure"); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertTrue(c.isSecure()); + assertEquals(NewCookie.SameSite.NONE, c.getSameSite()); + } + + @Test + public void testFromStringWithUnrecognizedSameSiteValue() { + NewCookie c = NewCookie.valueOf("foo=bar;SameSite=Unknown"); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertNull(c.getSameSite()); + } + + @Test + public void testFromStringIgnoresUnknownAttributes() { + NewCookie c = NewCookie.valueOf("foo=bar;Priority=High;Path=path"); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertEquals("path", c.getPath()); + } + + @Test + public void testToStringWithSameSite() { + NewCookie c = new NewCookie.Builder("foo").value("bar").path("path") + .sameSite(NewCookie.SameSite.STRICT).build(); + assertEquals("foo=bar;Path=path;SameSite=Strict;Version=1", c.toString()); + } + + @Test + public void testSameSiteRoundTrip() { + NewCookie c = NewCookie.valueOf( + new NewCookie.Builder("foo").value("bar").sameSite(NewCookie.SameSite.NONE).secure(true).build() + .toString()); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertTrue(c.isSecure()); + assertEquals(NewCookie.SameSite.NONE, c.getSameSite()); + } + } \ No newline at end of file From bebfaedbdf75a91153d100fc65922eab5ce7e8e8 Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 22 Oct 2026 01:18:35 +0100 Subject: [PATCH 2/4] CXF-9072: Skip empty Set-Cookie segments With the first name=value pair now taken as the cookie, an empty leading segment such as " ; foo=bar" would have become a cookie with an empty name. --- .../apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java | 5 ++++- .../cxf/jaxrs/impl/NewCookieHeaderProviderTest.java | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java index d22fd13e8a2..01814be97f6 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java @@ -65,6 +65,9 @@ public NewCookie fromString(String c) { String[] tokens = c.split(";"); for (String token : tokens) { String theToken = token.trim(); + if (theToken.isEmpty()) { + continue; + } int sepIndex = theToken.indexOf('='); String paramName = sepIndex != -1 ? theToken.substring(0, sepIndex) : theToken; @@ -153,7 +156,7 @@ public String toString(NewCookie value) { /** * Matches the SameSite attribute value case-insensitively; an unrecognized value is ignored, - * as RFC 6265bis requires. + * as the RFC 6265bis draft specifies. */ private static NewCookie.SameSite toSameSite(String attributeValue) { for (NewCookie.SameSite sameSite : NewCookie.SameSite.values()) { diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java index 8eb23e8b8c1..ee356ee14fd 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java @@ -186,6 +186,14 @@ public void testFromStringIgnoresUnknownAttributes() { assertEquals("path", c.getPath()); } + @Test + public void testFromStringSkipsEmptySegments() { + NewCookie c = NewCookie.valueOf(" ; foo=bar;;Path=path"); + assertEquals("foo", c.getName()); + assertEquals("bar", c.getValue()); + assertEquals("path", c.getPath()); + } + @Test public void testToStringWithSameSite() { NewCookie c = new NewCookie.Builder("foo").value("bar").path("path") From f616912253ba57889c94c4400b1b0fa924b9b51e Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 22 Oct 2026 06:14:15 +0100 Subject: [PATCH 3/4] CXF-9072: Always take the first Set-Cookie segment as the cookie Per RFC 6265 section 5.2 the first segment is the cookie's name=value pair whatever its name, and only the segments after it are attributes. A cookie named after an attribute (path=abc; Path=/) or named SameSite was parsed as attributes and rejected as malformed. A first segment without '=' is still rejected. --- .../jaxrs/impl/NewCookieHeaderProvider.java | 11 +++++----- .../impl/NewCookieHeaderProviderTest.java | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java index 01814be97f6..2691ba2d543 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java @@ -83,7 +83,12 @@ public NewCookie fromString(String c) { paramValue = stripQuotes(paramValue); } - if (paramName.equalsIgnoreCase(MAX_AGE)) { + if (name == null) { + // The first segment is always the cookie's name=value pair, whatever its name + // (RFC 6265, section 5.2); the segments after it are attributes, and unknown ones are ignored + name = paramName; + value = paramValue; + } else if (paramName.equalsIgnoreCase(MAX_AGE)) { maxAge = Integer.parseInt(paramValue); } else if (paramName.equalsIgnoreCase(PATH)) { path = paramValue; @@ -101,10 +106,6 @@ public NewCookie fromString(String c) { version = Integer.parseInt(paramValue); } else if (paramName.equalsIgnoreCase(SAME_SITE)) { sameSite = toSameSite(paramValue); - } else if (paramValue != null && name == null) { - // The cookie's own name=value pair comes first; unrecognized attributes after it are ignored - name = paramName; - value = paramValue; } } diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java index ee356ee14fd..e3555417951 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java @@ -186,6 +186,27 @@ public void testFromStringIgnoresUnknownAttributes() { assertEquals("path", c.getPath()); } + @Test + public void testFromStringWithCookieNamedAfterAnAttribute() { + NewCookie c = NewCookie.valueOf("path=abc; Path=/"); + assertEquals("path", c.getName()); + assertEquals("abc", c.getValue()); + assertEquals("/", c.getPath()); + } + + @Test + public void testFromStringWithCookieNamedSameSite() { + NewCookie c = NewCookie.valueOf("SameSite=Lax"); + assertEquals("SameSite", c.getName()); + assertEquals("Lax", c.getValue()); + assertNull(c.getSameSite()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromStringWithoutNameValuePairIsMalformed() { + NewCookie.valueOf("Secure; foo=bar"); + } + @Test public void testFromStringSkipsEmptySegments() { NewCookie c = NewCookie.valueOf(" ; foo=bar;;Path=path"); From 9641972d606adbe148a254be59b75541f2728dc3 Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 22 Oct 2026 16:23:20 +0100 Subject: [PATCH 4/4] CXF-9072: Write the SameSite value with StringUtils.capitalize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the switch with StringUtils.capitalize on the lower-cased enum name. Lower-case with Locale.ROOT: under a Turkish default locale, STRICT would otherwise become "Strıct" with a dotless i. --- .../cxf/jaxrs/impl/NewCookieHeaderProvider.java | 16 ++++------------ .../jaxrs/impl/NewCookieHeaderProviderTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java index 2691ba2d543..446fae28edc 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProvider.java @@ -20,10 +20,12 @@ package org.apache.cxf.jaxrs.impl; import java.util.Date; +import java.util.Locale; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate; +import org.apache.cxf.common.util.StringUtils; import org.apache.cxf.jaxrs.utils.HttpUtils; public class NewCookieHeaderProvider implements HeaderDelegate { @@ -148,7 +150,8 @@ public String toString(NewCookie value) { sb.append(';').append(HTTP_ONLY); } if (value.getSameSite() != null) { - sb.append(';').append(SAME_SITE).append('=').append(toAttributeValue(value.getSameSite())); + String sameSite = StringUtils.capitalize(value.getSameSite().name().toLowerCase(Locale.ROOT)); + sb.append(';').append(SAME_SITE).append('=').append(sameSite); } sb.append(';').append(VERSION).append('=').append(value.getVersion()); return sb.toString(); @@ -168,17 +171,6 @@ private static NewCookie.SameSite toSameSite(String attributeValue) { return null; } - private static String toAttributeValue(NewCookie.SameSite sameSite) { - switch (sameSite) { - case STRICT: - return "Strict"; - case LAX: - return "Lax"; - default: - return "None"; - } - } - /** * Append the input value string to the given buffer, wrapping it with * quotes if need be. diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java index e3555417951..bd05e5a22d9 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/NewCookieHeaderProviderTest.java @@ -20,6 +20,7 @@ package org.apache.cxf.jaxrs.impl; import java.util.Date; +import java.util.Locale; import jakarta.ws.rs.core.NewCookie; import org.apache.cxf.jaxrs.utils.HttpUtils; @@ -222,6 +223,19 @@ public void testToStringWithSameSite() { assertEquals("foo=bar;Path=path;SameSite=Strict;Version=1", c.toString()); } + @Test + public void testToStringWithSameSiteUnderTurkishLocale() { + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.forLanguageTag("tr-TR")); + try { + NewCookie c = new NewCookie.Builder("foo").value("bar") + .sameSite(NewCookie.SameSite.STRICT).build(); + assertEquals("foo=bar;SameSite=Strict;Version=1", c.toString()); + } finally { + Locale.setDefault(defaultLocale); + } + } + @Test public void testSameSiteRoundTrip() { NewCookie c = NewCookie.valueOf(