This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new 663cf11aa04 CXF-9072: Support the SameSite attribute in 
NewCookieHeaderProvider (#3616)
663cf11aa04 is described below

commit 663cf11aa042c68bf693b4724dd27a05225ea6f8
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.
---
 .../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

Reply via email to