From 4513365e72cc5271f34bd53c8e2d1fa9672567d8 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 23 Sep 2026 20:32:01 +0000 Subject: [PATCH] Keep a caller's cookie when the store refuses Set-Cookie --- .../intercept/Redirect30xInterceptor.java | 19 ++++++++++++++++--- .../intercept/RedirectCookieRotationTest.java | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index 39360acda..2225d2687 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -212,7 +212,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture requestBuilder.resetCookies(); } else { requestBuilder.setCookies(cookieStore == null - ? request.getCookies() : callersOwnCookies(request, response, cookieStore)); + ? request.getCookies() : callersOwnCookies(request, response, cookieStore, newUri)); } requestBuilder.setMethod(switchToGet ? GET : originalMethod) @@ -465,15 +465,28 @@ private enum BodyRepresentation { * and those the store still holds with the same value. A caller's cookie sharing only a name with a stored * one stays the caller's. */ - private List callersOwnCookies(Request request, HttpResponse response, CookieStore cookieStore) { + private List callersOwnCookies(Request request, HttpResponse response, CookieStore cookieStore, Uri newUri) { List cookies = request.getCookies(); if (cookies.isEmpty()) { return cookies; } + // Only what the store took counts: a Set-Cookie it refused, or one for another path, leaves the + // caller's cookie of that name in place. Set setByResponse = new HashSet<>(); + List next = null; for (String header : response.headers().getAll(SET_COOKIE)) { Cookie cookie = cookieDecoder.decode(header); - if (cookie != null) { + if (cookie == null) { + continue; + } + if (cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE && cookie.maxAge() <= 0) { + setByResponse.add(cookie.name()); + continue; + } + if (next == null) { + next = cookieStore.get(newUri); + } + if (holdsSameValue(next, cookie)) { setByResponse.add(cookie.name()); } } diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java index 097611568..41e1054bf 100644 --- a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/RedirectCookieRotationTest.java @@ -83,6 +83,12 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques case "/bounce-307": redirect(response, 307, null, "/home"); break; + case "/reject-domain": + redirect(response, HttpServletResponse.SC_FOUND, "SID=evil; Domain=example.org; Path=/", "/home"); + break; + case "/other-path": + redirect(response, HttpServletResponse.SC_FOUND, "SID=other; Path=/elsewhere", "/home"); + break; case "/see-other": redirect(response, HttpServletResponse.SC_SEE_OTHER, null, "/home"); break; @@ -159,6 +165,15 @@ void theCallersCookieStillBeatsAStoredOneOfTheSameName() throws Exception { assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/bounce")))); } + /** A Set-Cookie the store refused, or filed for another path, does not replace the caller's cookie. */ + @Test + void aSetCookieThatDoesNotReachTheNextHopLeavesTheCallersCookie() throws Exception { + assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/reject-domain"))), + "refused: Domain does not match"); + assertEquals("SID=mine", withCallerCookie("SID", "mine", client -> client.prepareGet(url("/other-path"))), + "stored for /elsewhere, not sent to /home"); + } + // Without a cookie store every cookie on the request is the caller's own. @Test