jetty / jetty/jetty.project

Positive Max-Age does not take precedence over an expired Expires attribute

Open
#15,760 5 comments 0 reactions 0 assignees View on GitHub
Bug
Dominant language
Java
Stars
4.1k
Forks
2k
Avg merge
3d 56m
Merged PRs (30d)
48

Description

**Jetty version(s)**

12.1.13-SNAPSHOT (reproduced at commit 9df38b9f3dab75f644e93685a8ee6be6ba5674ea)

The same expiration logic is still present in 12.1.14-SNAPSHOT on jetty-12.1.x at commit 255071490e5911115aecbaf7f670607c3bb6a5cd.

**Jetty Environment**

core

**HTTP version**

N/A — HttpCookieStore expiration is protocol-independent.

**Java version/vendor** (use: java -version)

Eclipse Temurin 21.0.12.1+1

**OS type/version**

Microsoft Windows 11 Home, version 10.0.26200 (build 26200)

**Description**

When a cookie contains both a positive Max-Age and an Expires value in the past, Jetty treats the cookie as expired immediately.

RFC 6265 section 5.3, step 3 gives Max-Age precedence over Expires when both attributes are present. Expires determines the expiry time only when the cookie does not contain a Max-Age attribute:

https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3

The behavior affects both HttpCookie.isExpired() and HttpCookieStore.Default.StoredHttpCookie.isExpired().

This can affect Jetty-based HTTP clients when servers send both attributes for compatibility and clock skew makes the absolute Expires value appear stale. Jetty may then discard an otherwise valid session or authentication cookie, causing subsequent requests to lose state or require authentication again.

For example, a cookie equivalent to:

Set-Cookie: sid=live; Max-Age=3600; Expires=Thu, 01 Jan 1970 00:00:00 GMT

should remain valid for one hour, but HttpCookieStore.Default.add() rejects it and match() returns no cookie.

Expected behavior:

- Max-Age=0 expires immediately.
- A positive Max-Age takes precedence over Expires.
- Stored cookies calculate the lifetime from their creation time.
- Jetty's existing negative Max-Age behavior remains unchanged.
- Expires is evaluated only when there is no positive Max-Age.

The proposed scope is to apply the precedence rule consistently in HttpCookie.isExpired() and HttpCookieStore.Default.StoredHttpCookie.isExpired(), without adding a special case to HttpCookieStore.add().

Compatibility note: a cookie with a positive Max-Age and a past Expires value would pass the expiration check instead of being discarded.

**How to reproduce?**

~~~java
HttpCookieStore store = new HttpCookieStore.Default();
URI uri = URI.create("https://example.com/");
HttpCookie cookie = HttpCookie.build("sid", "live")
.maxAge(3600)
.expires(Instant.EPOCH)
.build();

System.out.println(store.add(uri, cookie));
System.out.println(store.match(uri).size());
~~~

Expected:

~~~text
true
1
~~~

Actual:

~~~text
false
0
~~~

The existing negative Max-Age compatibility behavior described in #14651 is outside the scope of this issue.

**AI assistance disclosure**

English is not my native language. I used OpenAI Codex to check the English wording of this report and to help determine the appropriate implementation scope. I reproduced the issue locally and reviewed and verified the technical content before submission.

Contributor guide

Open the contributing guide

Research direction

Start with expiration handling in HttpCookie.isExpired() and HttpCookieStore.Default.StoredHttpCookie.isExpired(), then trace how HttpCookieStore.Default.add() and match() use those checks. Reproduce the supplied Max-Age and Expires example and add or update coverage for immediate expiry, positive Max-Age precedence, stored-cookie lifetime, and unchanged negative Max-Age behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.