Skip to content

Commit 988cc75

Browse files
committed
RFC 6265bis compliance for the cookie implementation
Enforces the __Secure- and __Host- cookie name prefixes, adds the SameSite attribute with the SameSite=None requires Secure rule, includes the host-only flag in cookie identity, applies the 4096-byte cookie and 1024-byte attribute size limits and the 400-day maximum lifetime, supports nameless cookies in both parsing and serialization, and prevents a non-secure connection from overwriting a stored secure cookie.
1 parent a890ec7 commit 988cc75

21 files changed

Lines changed: 963 additions & 39 deletions

httpclient5/src/main/java/org/apache/hc/client5/http/cookie/BasicCookieStore.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Date;
3535
import java.util.Iterator;
3636
import java.util.List;
37+
import java.util.Locale;
3738
import java.util.TreeSet;
3839
import java.util.concurrent.locks.ReadWriteLock;
3940
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -79,9 +80,29 @@ private void readObject(final ObjectInputStream stream) throws IOException, Clas
7980
*/
8081
@Override
8182
public void addCookie(final Cookie cookie) {
83+
addCookie(cookie, true);
84+
}
85+
86+
/**
87+
* Adds an {@link Cookie HTTP cookie} received over a connection whose security is described by
88+
* {@code secureConnection}, replacing any existing equivalent cookie. A cookie received over a
89+
* non-secure connection does not replace an existing secure cookie of the same identity. If the
90+
* given cookie has already expired it will not be added, but an existing equivalent cookie will
91+
* still be removed.
92+
*
93+
* @param cookie the {@link Cookie cookie} to be added
94+
* @param secureConnection whether the cookie was received over a secure connection
95+
*
96+
* @since 5.7
97+
*/
98+
@Override
99+
public void addCookie(final Cookie cookie, final boolean secureConnection) {
82100
if (cookie != null) {
83101
lock.writeLock().lock();
84102
try {
103+
if (!secureConnection && overlaysSecureCookie(cookie)) {
104+
return;
105+
}
85106
final Cookie oldCookie = cookies.ceiling(cookie);
86107
if (oldCookie != null && CookieIdentityComparator.INSTANCE.compare(oldCookie, cookie) == 0) {
87108
if (cookie instanceof SetCookie) {
@@ -103,6 +124,48 @@ public void addCookie(final Cookie cookie) {
103124
}
104125
}
105126

127+
private boolean overlaysSecureCookie(final Cookie cookie) {
128+
for (final Cookie existing : cookies) {
129+
if (existing.isSecure()
130+
&& namesMatch(existing.getName(), cookie.getName())
131+
&& (domainMatch(cookie.getDomain(), existing.getDomain())
132+
|| domainMatch(existing.getDomain(), cookie.getDomain()))
133+
&& pathMatch(cookie.getPath(), existing.getPath())) {
134+
return true;
135+
}
136+
}
137+
return false;
138+
}
139+
140+
private static boolean namesMatch(final String a, final String b) {
141+
return a == null ? b == null : a.equals(b);
142+
}
143+
144+
private static boolean domainMatch(final String host, final String domain) {
145+
if (host == null || domain == null) {
146+
return false;
147+
}
148+
final String h = host.toLowerCase(Locale.ROOT);
149+
String d = domain.toLowerCase(Locale.ROOT);
150+
if (d.startsWith(".")) {
151+
d = d.substring(1);
152+
}
153+
return h.equals(d)
154+
|| h.length() > d.length() && h.endsWith(d) && h.charAt(h.length() - d.length() - 1) == '.';
155+
}
156+
157+
private static boolean pathMatch(final String path, final String cookiePath) {
158+
final String p = path == null ? "/" : path;
159+
final String cp = cookiePath == null ? "/" : cookiePath;
160+
if (p.equals(cp)) {
161+
return true;
162+
}
163+
if (p.startsWith(cp)) {
164+
return cp.endsWith("/") || p.charAt(cp.length()) == '/';
165+
}
166+
return false;
167+
}
168+
106169
/**
107170
* Adds an array of {@link Cookie HTTP cookies}. Cookies are added individually and
108171
* in the given array order. If any of the given cookies has already expired it will

httpclient5/src/main/java/org/apache/hc/client5/http/cookie/Cookie.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public interface Cookie {
4747
String EXPIRES_ATTR = "expires";
4848
String HTTP_ONLY_ATTR = "httponly";
4949

50+
/**
51+
* @since 5.7
52+
*/
53+
String SAME_SITE_ATTR = "samesite";
54+
5055
/**
5156
* @since 5.0
5257
*/
@@ -181,5 +186,25 @@ default boolean isHttpOnly() {
181186
return false;
182187
}
183188

189+
/**
190+
* Returns the value of the {@code SameSite} attribute, or {@code null} if the attribute is
191+
* absent or unrecognized.
192+
*
193+
* @since 5.7
194+
*/
195+
default SameSite getSameSite() {
196+
return SameSite.fromString(getAttribute(SAME_SITE_ATTR));
197+
}
198+
199+
/**
200+
* Indicates whether this cookie is host-only, meaning it was set without a {@code Domain}
201+
* attribute and therefore applies only to the exact host that set it.
202+
*
203+
* @since 5.7
204+
*/
205+
default boolean isHostOnly() {
206+
return !containsAttribute(DOMAIN_ATTR);
207+
}
208+
184209
}
185210

httpclient5/src/main/java/org/apache/hc/client5/http/cookie/CookieIdentityComparator.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public int compare(final Cookie c1, final Cookie c2) {
8080
}
8181
res = p1.compareTo(p2);
8282
}
83+
if (res == 0) {
84+
res = Boolean.compare(c1.isHostOnly(), c2.isHostOnly());
85+
}
8386
return res;
8487
}
8588

httpclient5/src/main/java/org/apache/hc/client5/http/cookie/CookieStore.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ public interface CookieStore {
4747
*/
4848
void addCookie(Cookie cookie);
4949

50+
/**
51+
* Adds an {@link Cookie} received over a connection whose security is described by
52+
* {@code secureConnection}, replacing any existing equivalent cookie. A cookie received over a
53+
* non-secure connection must not replace an existing secure cookie of the same identity. The
54+
* default implementation ignores the security context and delegates to {@link #addCookie(Cookie)}.
55+
*
56+
* @param cookie the {@link Cookie cookie} to be added
57+
* @param secureConnection whether the cookie was received over a secure connection
58+
* @since 5.7
59+
*/
60+
default void addCookie(final Cookie cookie, final boolean secureConnection) {
61+
addCookie(cookie);
62+
}
63+
5064
/**
5165
* Returns all cookies contained in this store.
5266
*
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.cookie;
29+
30+
import java.util.Locale;
31+
32+
/**
33+
* Enumeration of the values of the {@code SameSite} cookie attribute.
34+
*
35+
* @since 5.7
36+
*/
37+
public enum SameSite {
38+
39+
/**
40+
* The cookie is only sent for same-site requests.
41+
*/
42+
STRICT("Strict"),
43+
44+
/**
45+
* The cookie is sent for same-site requests and top-level cross-site navigations.
46+
*/
47+
LAX("Lax"),
48+
49+
/**
50+
* The cookie is sent for all requests. A cookie with this value must also be secure.
51+
*/
52+
NONE("None");
53+
54+
private final String attributeValue;
55+
56+
SameSite(final String attributeValue) {
57+
this.attributeValue = attributeValue;
58+
}
59+
60+
/**
61+
* Returns the canonical attribute value as it appears in a {@code Set-Cookie} header.
62+
*/
63+
public String getAttributeValue() {
64+
return attributeValue;
65+
}
66+
67+
/**
68+
* Resolves a {@code SameSite} value from a raw attribute value using a case-insensitive match,
69+
* returning {@code null} when the value is absent or unrecognized.
70+
*/
71+
public static SameSite fromString(final String value) {
72+
if (value == null) {
73+
return null;
74+
}
75+
switch (value.trim().toLowerCase(Locale.ROOT)) {
76+
case "strict":
77+
return STRICT;
78+
case "lax":
79+
return LAX;
80+
case "none":
81+
return NONE;
82+
default:
83+
return null;
84+
}
85+
}
86+
87+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/BasicExpiresHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void parse(final SetCookie cookie, final String value)
8686
throw new MalformedCookieException("Invalid 'expires' attribute: "
8787
+ value);
8888
}
89-
cookie.setExpiryDate(expiry);
89+
cookie.setExpiryDate(CookieExpiryPolicy.cap(expiry, Instant.now()));
9090
}
9191

9292
@Override

httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/BasicMaxAgeHandler.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727
package org.apache.hc.client5.http.impl.cookie;
2828

29+
import java.math.BigInteger;
2930
import java.time.Instant;
3031

3132
import org.apache.hc.client5.http.cookie.CommonCookieAttributeHandler;
@@ -62,19 +63,19 @@ public void parse(final SetCookie cookie, final String value)
6263
if (value == null) {
6364
throw new MalformedCookieException("Missing value for 'max-age' attribute");
6465
}
65-
final int age;
66+
final BigInteger age;
6667
try {
67-
age = Integer.parseInt(value);
68+
age = new BigInteger(value);
6869
} catch (final NumberFormatException e) {
69-
throw new MalformedCookieException ("Invalid 'max-age' attribute: "
70-
+ value);
70+
throw new MalformedCookieException("Invalid 'max-age' attribute: " + value);
7171
}
72-
if (age <= 0) {
72+
if (age.signum() <= 0) {
7373
// RFC 6265 user-agent processing: delta-seconds <= 0 means immediate expiry.
7474
cookie.setExpiryDate(Instant.EPOCH);
7575
return;
7676
}
77-
cookie.setExpiryDate(Instant.now().plusSeconds(age));
77+
final BigInteger maxSeconds = BigInteger.valueOf(CookieExpiryPolicy.MAX_LIFETIME.getSeconds());
78+
cookie.setExpiryDate(Instant.now().plusSeconds(age.min(maxSeconds).longValueExact()));
7879
}
7980

8081
@Override
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.impl.cookie;
29+
30+
import org.apache.hc.client5.http.cookie.CommonCookieAttributeHandler;
31+
import org.apache.hc.client5.http.cookie.Cookie;
32+
import org.apache.hc.client5.http.cookie.CookieOrigin;
33+
import org.apache.hc.client5.http.cookie.MalformedCookieException;
34+
import org.apache.hc.client5.http.cookie.SameSite;
35+
import org.apache.hc.client5.http.cookie.SetCookie;
36+
import org.apache.hc.core5.annotation.Contract;
37+
import org.apache.hc.core5.annotation.ThreadingBehavior;
38+
import org.apache.hc.core5.util.Args;
39+
40+
/**
41+
* Cookie {@code SameSite} attribute handler. The raw attribute value is retained by the cookie
42+
* specification and exposed through {@link Cookie#getSameSite()}; this handler enforces that a
43+
* {@code SameSite=None} cookie must also be secure.
44+
*
45+
* @since 5.7
46+
*/
47+
@Contract(threading = ThreadingBehavior.STATELESS)
48+
public class BasicSameSiteHandler extends AbstractCookieAttributeHandler implements CommonCookieAttributeHandler {
49+
50+
/**
51+
* Default instance of {@link BasicSameSiteHandler}.
52+
*/
53+
public static final BasicSameSiteHandler INSTANCE = new BasicSameSiteHandler();
54+
55+
public BasicSameSiteHandler() {
56+
super();
57+
}
58+
59+
@Override
60+
public void parse(final SetCookie cookie, final String value) throws MalformedCookieException {
61+
Args.notNull(cookie, "Cookie");
62+
}
63+
64+
@Override
65+
public void validate(final Cookie cookie, final CookieOrigin origin) throws MalformedCookieException {
66+
Args.notNull(cookie, "Cookie");
67+
if (SameSite.NONE == cookie.getSameSite() && !cookie.isSecure()) {
68+
throw new MalformedCookieException("Cookie '" + cookie.getName()
69+
+ "' has SameSite=None but is not marked secure");
70+
}
71+
}
72+
73+
@Override
74+
public String getAttributeName() {
75+
return Cookie.SAME_SITE_ATTR;
76+
}
77+
78+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/BasicSecureHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ public void parse(final SetCookie cookie, final String value)
6161
cookie.setSecure(true);
6262
}
6363

64+
@Override
65+
public void validate(final Cookie cookie, final CookieOrigin origin)
66+
throws MalformedCookieException {
67+
Args.notNull(cookie, "Cookie");
68+
Args.notNull(origin, "Cookie origin");
69+
if (cookie.isSecure() && !origin.isSecure()) {
70+
throw new MalformedCookieException("Cookie '" + cookie.getName()
71+
+ "' is marked secure but was received over a non-secure connection");
72+
}
73+
}
74+
6475
@Override
6576
public boolean match(final Cookie cookie, final CookieOrigin origin) {
6677
Args.notNull(cookie, "Cookie");

0 commit comments

Comments
 (0)