This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
new 0cb257180a9 CXF-9072: Support the SameSite attribute in
NewCookieHeaderProvider (#3616)
0cb257180a9 is described below
commit 0cb257180a9cd0e268c9ec09f43ac907672124c5
Author: Sharang Gupta <[email protected]>
AuthorDate: Thu Oct 22 22:08:11 2026 +0100
CXF-9072: Support the SameSite attribute in NewCookieHeaderProvider (#3616)
* 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().
* 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.
* 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.
* CXF-9072: Write the SameSite value with StringUtils.capitalize
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.
(cherry picked from commit 663cf11aa042c68bf693b4724dd27a05225ea6f8)
---
.../cxf/jaxrs/impl/NewCookieHeaderProvider.java | 39 +++++++--
.../jaxrs/impl/NewCookieHeaderProviderTest.java | 96 ++++++++++++++++++++++
2 files changed, 130 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 e4759978823..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<NewCookie> {
@@ -36,6 +38,7 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
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,10 +62,14 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
Date expires = null;
boolean httpOnly = false;
int version = Cookie.DEFAULT_VERSION;
+ NewCookie.SameSite sameSite = null;
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;
@@ -78,7 +85,12 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
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;
@@ -94,9 +106,8 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
httpOnly = true;
} else if (paramName.equalsIgnoreCase(VERSION)) {
version = Integer.parseInt(paramValue);
- } else if (paramValue != null) {
- name = paramName;
- value = paramValue;
+ } else if (paramName.equalsIgnoreCase(SAME_SITE)) {
+ sameSite = toSameSite(paramValue);
}
}
@@ -104,7 +115,8 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
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 +149,28 @@ public class NewCookieHeaderProvider implements
HeaderDelegate<NewCookie> {
if (value.isHttpOnly()) {
sb.append(';').append(HTTP_ONLY);
}
+ if (value.getSameSite() != null) {
+ 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();
}
+ /**
+ * Matches the SameSite attribute value case-insensitively; an
unrecognized value is ignored,
+ * as the RFC 6265bis draft specifies.
+ */
+ private static NewCookie.SameSite toSameSite(String attributeValue) {
+ for (NewCookie.SameSite sameSite : NewCookie.SameSite.values()) {
+ if (sameSite.name().equalsIgnoreCase(attributeValue)) {
+ return sameSite;
+ }
+ }
+ return null;
+ }
+
/**
* 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..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;
@@ -28,6 +29,7 @@ import org.junit.Test;
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 +153,98 @@ public class NewCookieHeaderProviderTest {
+ "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 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");
+ 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 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(
+ 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