-
Notifications
You must be signed in to change notification settings - Fork 6.1k
8351983: HttpCookie Parser Incorrectly Handles Cookies with Expires Attribute #25636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
42144ef
76d41e7
9d4957c
faed1a4
8619aa1
b0ab727
cf25c05
2ab5478
53d5c25
ef97570
e44ccf9
9a495d7
b221131
75690ec
9ee697d
a6631ae
8d34be8
3b71e4f
6eba52a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. | ||
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. | ||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
* | ||
* This code is free software; you can redistribute it and/or modify it | ||
|
@@ -51,10 +51,12 @@ | |
* <i>http://www.ietf.org/rfc/rfc2965.txt</i></a> | ||
* </blockquote> | ||
* | ||
* <p> HttpCookie class can accept all these 3 forms of syntax. | ||
* <p> HttpCookie class can accept all these 3 forms of syntax. This class also provides | ||
* partial support for RFC 6265. | ||
* | ||
* @spec https://www.rfc-editor.org/info/rfc2109 RFC 2109: HTTP State Management Mechanism | ||
* @spec https://www.rfc-editor.org/info/rfc2965 RFC 2965: HTTP State Management Mechanism | ||
* @spec https://www.rfc-editor.org/info/rfc6265 RFC 6265: HTTP State Management Mechanism | ||
* @author Edward Wang | ||
* @since 1.6 | ||
*/ | ||
|
@@ -185,14 +187,14 @@ private HttpCookie(String name, String value, String header) { | |
* if the header string is {@code null} | ||
*/ | ||
public static List<HttpCookie> parse(String header) { | ||
return parse(header, false); | ||
return parse(header, false, -1L); | ||
} | ||
|
||
// Private version of parse() that will store the original header used to | ||
// create the cookie, in the cookie itself. This can be useful for filtering | ||
// Set-Cookie[2] headers, using the internal parsing logic defined in this | ||
// class. | ||
private static List<HttpCookie> parse(String header, boolean retainHeader) { | ||
// class. Also, allows for testing by specifying the creation time. | ||
static List<HttpCookie> parse(String header, boolean retainHeader, long currentTimeMillis) { | ||
|
||
int version = guessCookieVersion(header); | ||
|
||
|
@@ -209,7 +211,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) { | |
// so the parse logic is slightly different | ||
if (version == 0) { | ||
// Netscape draft cookie | ||
HttpCookie cookie = parseInternal(header, retainHeader); | ||
HttpCookie cookie = parseInternal(header, retainHeader, currentTimeMillis); | ||
cookie.setVersion(0); | ||
cookies.add(cookie); | ||
} else { | ||
|
@@ -218,7 +220,7 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) { | |
// it'll separate them with comma | ||
List<String> cookieStrings = splitMultiCookies(header); | ||
for (String cookieStr : cookieStrings) { | ||
HttpCookie cookie = parseInternal(cookieStr, retainHeader); | ||
HttpCookie cookie = parseInternal(cookieStr, retainHeader, currentTimeMillis); | ||
cookie.setVersion(1); | ||
cookies.add(cookie); | ||
} | ||
|
@@ -230,20 +232,27 @@ private static List<HttpCookie> parse(String header, boolean retainHeader) { | |
// ---------------- Public operations -------------- | ||
|
||
/** | ||
* Reports whether this HTTP cookie has expired or not. | ||
* Reports whether this HTTP cookie has expired or not. This is | ||
* based on whether {@link #getMaxAge()} seconds have elapsed since | ||
* this object was created. | ||
* | ||
* @return {@code true} to indicate this HTTP cookie has expired; | ||
* otherwise, {@code false} | ||
*/ | ||
public boolean hasExpired() { | ||
return hasExpired(System.currentTimeMillis()); | ||
} | ||
|
||
// PP for testing | ||
boolean hasExpired(long currentTimeMillis) { | ||
if (maxAge == 0) return true; | ||
|
||
// if not specify max-age, this cookie should be | ||
// discarded when user agent is to be closed, but | ||
// it is not expired. | ||
if (maxAge < 0) return false; | ||
|
||
long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000; | ||
long deltaSecond = (currentTimeMillis - whenCreated) / 1000; | ||
if (deltaSecond > maxAge) | ||
return true; | ||
else | ||
|
@@ -411,8 +420,19 @@ public void setMaxAge(long expiry) { | |
} | ||
|
||
/** | ||
* Returns the maximum age of the cookie, specified in seconds. By default, | ||
* {@code -1} indicating the cookie will persist until browser shutdown. | ||
* Returns the maximum age of the cookie, specified in seconds from the time | ||
* the object was created. By default, {@code -1} indicating the cookie will | ||
* persist until browser shutdown. | ||
* | ||
* The value of this attribute is determined by the following steps, in line | ||
* with RFC 6265: | ||
* | ||
* <ol><li>If {@link #setMaxAge(long)} was called, return the value set.</li> | ||
* <li>If previous step failed, and a {@code Max-Age} attribute was parsed | ||
* then return that value.</li> | ||
* <li>If previous step failed, and an {@code Expires} attribute was parsed | ||
* then the maxAge calculated at parsing time from that date, is returned</li> | ||
* <li>If previous step failed, then return {@code -1}.</li></ol> | ||
* | ||
* @return an integer specifying the maximum age of the cookie in seconds | ||
* | ||
|
@@ -810,10 +830,13 @@ private static boolean isToken(String value) { | |
* if header string violates the cookie specification | ||
*/ | ||
private static HttpCookie parseInternal(String header, | ||
boolean retainHeader) | ||
boolean retainHeader, | ||
long currentTimeMillis) | ||
{ | ||
HttpCookie cookie = null; | ||
String namevaluePair = null; | ||
if (currentTimeMillis == -1L) | ||
currentTimeMillis = System.currentTimeMillis(); | ||
|
||
StringTokenizer tokenizer = new StringTokenizer(header, ";"); | ||
|
||
|
@@ -828,10 +851,11 @@ private static HttpCookie parseInternal(String header, | |
if (retainHeader) | ||
cookie = new HttpCookie(name, | ||
stripOffSurroundingQuote(value), | ||
header); | ||
header, currentTimeMillis); | ||
else | ||
cookie = new HttpCookie(name, | ||
stripOffSurroundingQuote(value)); | ||
stripOffSurroundingQuote(value), | ||
null, currentTimeMillis); | ||
} else { | ||
// no "=" in name-value pair; it's an error | ||
throw new IllegalArgumentException("Invalid cookie name-value pair"); | ||
|
@@ -840,6 +864,10 @@ private static HttpCookie parseInternal(String header, | |
throw new IllegalArgumentException("Empty cookie header string"); | ||
} | ||
|
||
// Attributes that require special handling | ||
String expiresValue = null; | ||
String maxAgeValue = null; | ||
|
||
// remaining name-value pairs are cookie's attributes | ||
while (tokenizer.hasMoreTokens()) { | ||
namevaluePair = tokenizer.nextToken(); | ||
|
@@ -852,10 +880,19 @@ private static HttpCookie parseInternal(String header, | |
name = namevaluePair.trim(); | ||
value = null; | ||
} | ||
if (name.equalsIgnoreCase("max-age") && maxAgeValue == null) { | ||
maxAgeValue = value; | ||
continue; | ||
} | ||
if (name.equalsIgnoreCase("expires") && expiresValue == null) { | ||
expiresValue = value; | ||
continue; | ||
} | ||
|
||
// assign attribute to cookie | ||
assignAttribute(cookie, name, value); | ||
} | ||
assignMaxAgeAttribute(cookie, expiresValue, maxAgeValue); | ||
Comment on lines
+883
to
+895
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Michael-Mc-Mahon, instead of making an exception for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just converting from Map to List wouldn't be enough. The problem is that both attribute types need to be handled together. You could change the attribute name recognition to some kind of pattern match to recognise either of them. Then you need to know which of them was set and what their values were. Maybe, I could at least use the assignor pattern to recognise the two attributes and limit the special code to just actioning the values. I'll take a look at that now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the last commit (b221131) just worsened things – now the logic is spread across I agree that introducing a smarter data structure and iteration scheme to Apologies for the inconvenience and thanks so much for your patient cooperation. 🙇 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree. I will revert it. The old version was clearer. |
||
|
||
return cookie; | ||
} | ||
|
@@ -903,20 +940,6 @@ public void assign(HttpCookie cookie, | |
cookie.setDomain(attrValue); | ||
} | ||
}); | ||
assignors.put("max-age", new CookieAttributeAssignor(){ | ||
public void assign(HttpCookie cookie, | ||
String attrName, | ||
String attrValue) { | ||
try { | ||
long maxage = Long.parseLong(attrValue); | ||
if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) | ||
cookie.setMaxAge(maxage); | ||
} catch (NumberFormatException ignored) { | ||
throw new IllegalArgumentException( | ||
"Illegal cookie max-age attribute"); | ||
} | ||
} | ||
}); | ||
assignors.put("path", new CookieAttributeAssignor(){ | ||
public void assign(HttpCookie cookie, | ||
String attrName, | ||
|
@@ -959,17 +982,37 @@ public void assign(HttpCookie cookie, | |
} | ||
} | ||
}); | ||
assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only | ||
public void assign(HttpCookie cookie, | ||
String attrName, | ||
String attrValue) { | ||
if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) { | ||
long delta = cookie.expiryDate2DeltaSeconds(attrValue); | ||
cookie.setMaxAge(delta > 0 ? delta : 0); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
private static void assignMaxAgeAttribute(HttpCookie cookie, | ||
String expiresValue, | ||
String maxAgeValue) | ||
{ | ||
if (cookie.getMaxAge() != MAX_AGE_UNSPECIFIED) | ||
return; | ||
if (expiresValue == null && maxAgeValue == null) | ||
return; | ||
|
||
// strip off the surrounding "-sign if there's any | ||
expiresValue = stripOffSurroundingQuote(expiresValue); | ||
maxAgeValue = stripOffSurroundingQuote(maxAgeValue); | ||
|
||
try { | ||
if (maxAgeValue != null) { | ||
long maxAge = Long.parseLong(maxAgeValue); | ||
cookie.maxAge = maxAge; | ||
return; | ||
} | ||
} catch (NumberFormatException ignored) {} | ||
|
||
try { | ||
if (expiresValue != null) { | ||
long delta = cookie.expiryDate2DeltaSeconds(expiresValue); | ||
cookie.maxAge = (delta > 0 ? delta : 0); | ||
} | ||
} catch (NumberFormatException ignored) {} | ||
} | ||
|
||
private static void assignAttribute(HttpCookie cookie, | ||
String attrName, | ||
String attrValue) | ||
|
@@ -989,7 +1032,7 @@ private static void assignAttribute(HttpCookie cookie, | |
SharedSecrets.setJavaNetHttpCookieAccess( | ||
new JavaNetHttpCookieAccess() { | ||
public List<HttpCookie> parse(String header) { | ||
return HttpCookie.parse(header, true); | ||
return HttpCookie.parse(header, true, -1L); | ||
} | ||
|
||
public String header(HttpCookie cookie) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. | ||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
* | ||
* This code is free software; you can redistribute it and/or modify it | ||
* under the terms of the GNU General Public License version 2 only, as | ||
* published by the Free Software Foundation. | ||
* | ||
* This code is distributed in the hope that it will be useful, but WITHOUT | ||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
* version 2 for more details (a copy is included in the LICENSE file that | ||
* accompanied this code). | ||
* | ||
* You should have received a copy of the GNU General Public License version | ||
* 2 along with this work; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
* | ||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | ||
* or visit www.oracle.com if you need additional information or have any | ||
* questions. | ||
*/ | ||
|
||
/* | ||
* @test | ||
* @bug 8351983 | ||
* @summary HttpCookie Parser Incorrectly Handles Cookies with Expires Attribute | ||
* @run testng java.base/java.net.MaxAgeExpires | ||
*/ | ||
public class MaxAgeExpiresDriver { | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copyright year needs to be updated.