Skip to content

Commit 116b854

Browse files
8351983: HttpCookie Parser Incorrectly Handles Cookies with Expires Attribute
Reviewed-by: dfuchs, vyazici
1 parent fdfc557 commit 116b854

File tree

3 files changed

+250
-39
lines changed

3 files changed

+250
-39
lines changed

src/java.base/share/classes/java/net/HttpCookie.java

Lines changed: 82 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -51,10 +51,12 @@
5151
* <i>http://www.ietf.org/rfc/rfc2965.txt</i></a>
5252
* </blockquote>
5353
*
54-
* <p> HttpCookie class can accept all these 3 forms of syntax.
54+
* <p> HttpCookie class can accept all these 3 forms of syntax. This class also provides
55+
* partial support for RFC 6265.
5556
*
5657
* @spec https://www.rfc-editor.org/info/rfc2109 RFC 2109: HTTP State Management Mechanism
5758
* @spec https://www.rfc-editor.org/info/rfc2965 RFC 2965: HTTP State Management Mechanism
59+
* @spec https://www.rfc-editor.org/info/rfc6265 RFC 6265: HTTP State Management Mechanism
5860
* @author Edward Wang
5961
* @since 1.6
6062
*/
@@ -185,14 +187,14 @@ private HttpCookie(String name, String value, String header) {
185187
* if the header string is {@code null}
186188
*/
187189
public static List<HttpCookie> parse(String header) {
188-
return parse(header, false);
190+
return parse(header, false, -1L);
189191
}
190192

191193
// Private version of parse() that will store the original header used to
192194
// create the cookie, in the cookie itself. This can be useful for filtering
193195
// Set-Cookie[2] headers, using the internal parsing logic defined in this
194-
// class.
195-
private static List<HttpCookie> parse(String header, boolean retainHeader) {
196+
// class. Also, allows for testing by specifying the creation time.
197+
static List<HttpCookie> parse(String header, boolean retainHeader, long currentTimeMillis) {
196198

197199
int version = guessCookieVersion(header);
198200

@@ -209,7 +211,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
209211
// so the parse logic is slightly different
210212
if (version == 0) {
211213
// Netscape draft cookie
212-
HttpCookie cookie = parseInternal(header, retainHeader);
214+
HttpCookie cookie = parseInternal(header, retainHeader, currentTimeMillis);
213215
cookie.setVersion(0);
214216
cookies.add(cookie);
215217
} else {
@@ -218,7 +220,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
218220
// it'll separate them with comma
219221
List<String> cookieStrings = splitMultiCookies(header);
220222
for (String cookieStr : cookieStrings) {
221-
HttpCookie cookie = parseInternal(cookieStr, retainHeader);
223+
HttpCookie cookie = parseInternal(cookieStr, retainHeader, currentTimeMillis);
222224
cookie.setVersion(1);
223225
cookies.add(cookie);
224226
}
@@ -230,20 +232,27 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) {
230232
// ---------------- Public operations --------------
231233

232234
/**
233-
* Reports whether this HTTP cookie has expired or not.
235+
* Reports whether this HTTP cookie has expired or not. This is
236+
* based on whether {@link #getMaxAge()} seconds have elapsed since
237+
* this object was created.
234238
*
235239
* @return {@code true} to indicate this HTTP cookie has expired;
236240
* otherwise, {@code false}
237241
*/
238242
public boolean hasExpired() {
243+
return hasExpired(System.currentTimeMillis());
244+
}
245+
246+
// PP for testing
247+
boolean hasExpired(long currentTimeMillis) {
239248
if (maxAge == 0) return true;
240249

241250
// if not specify max-age, this cookie should be
242251
// discarded when user agent is to be closed, but
243252
// it is not expired.
244253
if (maxAge < 0) return false;
245254

246-
long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000;
255+
long deltaSecond = (currentTimeMillis - whenCreated) / 1000;
247256
if (deltaSecond > maxAge)
248257
return true;
249258
else
@@ -411,8 +420,19 @@ public void setMaxAge(long expiry) {
411420
}
412421

413422
/**
414-
* Returns the maximum age of the cookie, specified in seconds. By default,
415-
* {@code -1} indicating the cookie will persist until browser shutdown.
423+
* Returns the maximum age of the cookie, specified in seconds from the time
424+
* the object was created. By default, {@code -1} indicating the cookie will
425+
* persist until browser shutdown.
426+
*
427+
* The value of this attribute is determined by the following steps, in line
428+
* with RFC 6265:
429+
*
430+
* <ol><li>If {@link #setMaxAge(long)} was called, return the value set.</li>
431+
* <li>If previous step failed, and a {@code Max-Age} attribute was parsed
432+
* then return that value.</li>
433+
* <li>If previous step failed, and an {@code Expires} attribute was parsed
434+
* then the maxAge calculated at parsing time from that date, is returned</li>
435+
* <li>If previous step failed, then return {@code -1}.</li></ol>
416436
*
417437
* @return an integer specifying the maximum age of the cookie in seconds
418438
*
@@ -810,10 +830,13 @@ private static boolean isToken(String value) {
810830
* if header string violates the cookie specification
811831
*/
812832
private static HttpCookie parseInternal(String header,
813-
boolean retainHeader)
833+
boolean retainHeader,
834+
long currentTimeMillis)
814835
{
815836
HttpCookie cookie = null;
816837
String namevaluePair = null;
838+
if (currentTimeMillis == -1L)
839+
currentTimeMillis = System.currentTimeMillis();
817840

818841
StringTokenizer tokenizer = new StringTokenizer(header, ";");
819842

@@ -828,10 +851,11 @@ private static HttpCookie parseInternal(String header,
828851
if (retainHeader)
829852
cookie = new HttpCookie(name,
830853
stripOffSurroundingQuote(value),
831-
header);
854+
header, currentTimeMillis);
832855
else
833856
cookie = new HttpCookie(name,
834-
stripOffSurroundingQuote(value));
857+
stripOffSurroundingQuote(value),
858+
null, currentTimeMillis);
835859
} else {
836860
// no "=" in name-value pair; it's an error
837861
throw new IllegalArgumentException("Invalid cookie name-value pair");
@@ -840,6 +864,10 @@ private static HttpCookie parseInternal(String header,
840864
throw new IllegalArgumentException("Empty cookie header string");
841865
}
842866

867+
// Attributes that require special handling
868+
String expiresValue = null;
869+
String maxAgeValue = null;
870+
843871
// remaining name-value pairs are cookie's attributes
844872
while (tokenizer.hasMoreTokens()) {
845873
namevaluePair = tokenizer.nextToken();
@@ -852,10 +880,19 @@ private static HttpCookie parseInternal(String header,
852880
name = namevaluePair.trim();
853881
value = null;
854882
}
883+
if (name.equalsIgnoreCase("max-age") && maxAgeValue == null) {
884+
maxAgeValue = value;
885+
continue;
886+
}
887+
if (name.equalsIgnoreCase("expires") && expiresValue == null) {
888+
expiresValue = value;
889+
continue;
890+
}
855891

856892
// assign attribute to cookie
857893
assignAttribute(cookie, name, value);
858894
}
895+
assignMaxAgeAttribute(cookie, expiresValue, maxAgeValue);
859896

860897
return cookie;
861898
}
@@ -903,20 +940,6 @@ public void assign(HttpCookie cookie,
903940
cookie.setDomain(attrValue);
904941
}
905942
});
906-
assignors.put("max-age", new CookieAttributeAssignor(){
907-
public void assign(HttpCookie cookie,
908-
String attrName,
909-
String attrValue) {
910-
try {
911-
long maxage = Long.parseLong(attrValue);
912-
if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED)
913-
cookie.setMaxAge(maxage);
914-
} catch (NumberFormatException ignored) {
915-
throw new IllegalArgumentException(
916-
"Illegal cookie max-age attribute");
917-
}
918-
}
919-
});
920943
assignors.put("path", new CookieAttributeAssignor(){
921944
public void assign(HttpCookie cookie,
922945
String attrName,
@@ -959,17 +982,37 @@ public void assign(HttpCookie cookie,
959982
}
960983
}
961984
});
962-
assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only
963-
public void assign(HttpCookie cookie,
964-
String attrName,
965-
String attrValue) {
966-
if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) {
967-
long delta = cookie.expiryDate2DeltaSeconds(attrValue);
968-
cookie.setMaxAge(delta > 0 ? delta : 0);
969-
}
970-
}
971-
});
972985
}
986+
987+
private static void assignMaxAgeAttribute(HttpCookie cookie,
988+
String expiresValue,
989+
String maxAgeValue)
990+
{
991+
if (cookie.getMaxAge() != MAX_AGE_UNSPECIFIED)
992+
return;
993+
if (expiresValue == null && maxAgeValue == null)
994+
return;
995+
996+
// strip off the surrounding "-sign if there's any
997+
expiresValue = stripOffSurroundingQuote(expiresValue);
998+
maxAgeValue = stripOffSurroundingQuote(maxAgeValue);
999+
1000+
try {
1001+
if (maxAgeValue != null) {
1002+
long maxAge = Long.parseLong(maxAgeValue);
1003+
cookie.maxAge = maxAge;
1004+
return;
1005+
}
1006+
} catch (NumberFormatException ignored) {}
1007+
1008+
try {
1009+
if (expiresValue != null) {
1010+
long delta = cookie.expiryDate2DeltaSeconds(expiresValue);
1011+
cookie.maxAge = (delta > 0 ? delta : 0);
1012+
}
1013+
} catch (NumberFormatException ignored) {}
1014+
}
1015+
9731016
private static void assignAttribute(HttpCookie cookie,
9741017
String attrName,
9751018
String attrValue)
@@ -989,7 +1032,7 @@ private static void assignAttribute(HttpCookie cookie,
9891032
SharedSecrets.setJavaNetHttpCookieAccess(
9901033
new JavaNetHttpCookieAccess() {
9911034
public List<HttpCookie> parse(String header) {
992-
return HttpCookie.parse(header, true);
1035+
return HttpCookie.parse(header, true, -1L);
9931036
}
9941037

9951038
public String header(HttpCookie cookie) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8351983
27+
* @summary HttpCookie Parser Incorrectly Handles Cookies with Expires Attribute
28+
* @run testng java.base/java.net.MaxAgeExpires
29+
*/
30+
public class MaxAgeExpiresDriver {
31+
}

0 commit comments

Comments
 (0)