Skip to content

fix(netty): keep caller Cookie headers as written on redirects and auth retries - #2360

Open
mkurz wants to merge 1 commit into
AsyncHttpClient:mainfrom
mkurz:fix/raw-cookie-hop-reconciliation
Open

mkurz wants to merge 1 commit into
AsyncHttpClient:mainfrom
mkurz:fix/raw-cookie-hop-reconciliation

Conversation

@mkurz

@mkurz mkurz commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • On a redirect or an authentication retry, keep a caller-set Cookie header as text, removing only the pairs that the response replaced, instead of decoding it leniently and re-encoding it strictly.
  • Find those pairs by splitting at every ;, as NettyRequestFactory.mergeCookies does, so that a quote in front of a replaced pair cannot hide it and resend a stale session.
  • No public API change. Follow-up to fd97636 (GHSA-2jwh-9rmr-j4xf).

Problem

Since fd97636, a caller-set Cookie header is kept when the cookie store also contributes cookies to a hop, with the pairs that the response replaced removed. The cookie store is enabled by default (ThreadSafeCookieStore), so this path covers ordinary clients. It applies to redirects and to authentication retries: 401 (Basic, Digest, NTLM) and proxy 407.

To find the replaced pairs, CallerCookies decoded the caller's header leniently and re-encoded the remaining pairs strictly. That broke headers that the first request had sent without complaint:

  • A value that the strict encoder rejects failed the redirect or retry, although the initial request had been sent.
  • Pairs could be reordered, and valueless or unbalanced-quote pairs were dropped.

Change

  • Keep the caller-set header as text: remove only the pairs whose names the response replaced. Cookie objects are still encoded normally, and caller cookies are still stripped at cross-origin redirect boundaries.
  • Split at every ;, as mergeCookies does when it decides which names the caller set. RFC 6265 does not allow a ; in a cookie value, quoted or not. Keeping the text while splitting like the lenient decoder, which treats a double quote as opening a quoted string, would be unsafe. A header such as X=a"b; SID=old or X="a; SID=old would become a single pair named X. A SID that a redirect, 401 or 407 response rotated, or deleted with Max-Age=0, would stay in the header. mergeCookies would still count it as the caller's and drop the store's new value, so the next request would carry the stale session. Tests pin these cases.
  • Cut out only the replaced pairs and keep the rest as written, separators included, so prefs={"a":"x;y"} or A=1;B=2 is not rewritten.
  • Return a header that loses no pair unchanged, instead of re-joining it with "; ".
  • Restore the Javadoc of CallerCookies.of, and fix a stale comment in Redirect30xInterceptor.

Compatibility

There is no public API change. On redirects and authentication retries, a caller-set Cookie header now reaches the next hop as written, minus the pairs the response replaced. Before, it could be reordered, lose pairs, or fail the hop.

AI disclosure

OpenAI Codex and Claude Code on behalf of Matthias Kurz. The change was written in two steps, the first with Codex and the second with Claude Code, and squashed into one commit, which carries both Co-Authored-By trailers per AGENTS.md.

Test plan

New tests in RedirectCookieRotationTest and AuthRetryCookieTest cover:

  • redirects with cookie rotation, and Digest, proxy 407 and NTLM (both rounds) retries;
  • the lax encoder, several caller Cookie headers, and a raw pair deleted with Max-Age=0;
  • quotes in front of a replaced pair, and byte-exact kept text.

Runs:

  • ./mvnw -B -ntp -Dgpg.skip=true clean verify on JDK 11: 1,808 tests, 0 failures, 0 errors, 22 skipped; Revapi passed. No test-skipping flags were used.
  • The cookie and redirect tests, compiled on JDK 11 and run on JDK 17, 21 and 25 (Maven Surefire's -Djvm): 245 tests each, 0 failures.
  • All four open AHC changes merged together (raw Cookie headers on retries, ResponseBodyControl.execute, suspend/resume order, demand-bounded decompression): they merge without conflicts, together and in every pair, and ./mvnw -B -ntp -Dgpg.skip=true clean install on JDK 11 passes with 1,853 tests, 0 failures, 0 errors, 22 skipped; Revapi passed.
  • Play WS against that merged build: a new Play WS test sends Cookie: z=1; flash=stale; a=2; flag through a same-origin redirect that sets flash, with the cookie store enabled. The next hop receives z=1; a=2; flag; flash=redirect-cookie. Against AHC without this change, the valueless flag is dropped and the test fails.

A redirect or authentication retry decoded a caller-set Cookie header
leniently, then re-encoded it strictly, whenever the cookie store also
contributed cookies to the hop. Values accepted on the first hop could
therefore fail on the next one, pairs could be reordered, and valueless
or unbalanced-quote pairs were dropped.

Keep caller-set header pairs as text and cut out only the pairs whose
names the response replaced. The rest stays as written, separators
included, so prefs={"a":"x;y"} or A=1;B=2 is not rewritten, and a
header that loses no pair is returned unchanged. Cookie objects are
still encoded normally, and caller cookies are still stripped at
cross-origin redirect boundaries.

Find the pairs by splitting on every ';', as
NettyRequestFactory.mergeCookies does when it decides which names the
caller set; RFC 6265 does not allow a ';' in a cookie value, quoted or
not. Treating a double quote as opening a quoted string would let a
header such as X=a"b; SID=old hide a SID that a redirect, 401 or 407
response rotated or deleted, so that the next request resent the stale
session.

Restore the Javadoc of CallerCookies.of, and fix the comment on the
Cookie header in Redirect30xInterceptor, which still said the caller's
pairs travel in the cookie list. Add tests for redirects with cookie
rotation, Digest, proxy and NTLM retries, the lax encoder, several
caller Cookie headers, a raw pair deleted with Max-Age=0, quotes in
front of a replaced pair, and byte-exact kept text.

Follow-up to fd97636 (GHSA-2jwh-9rmr-j4xf).

OpenAI Codex and Claude Code on behalf of Matthias Kurz

Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant