AsyncHttpClient wave (10 GHSAs): replay writes one host's request into another host's TLS tunnel¶
Wave in one line: Client-side credential-crossing primitives — stale-target replay writes one host's request into another host's TLS tunnel, digest downgrade leaks cleartext passwords, plaintext HTTP overwrites Secure cookies, and NTLM pool keys keep leaking identities
Source: hourly offensive-security scan of GitHub Security Advisories, 2026-10-08 (wave published 2026-10-08T16:08–16:50Z, ten advisories for org.asynchttpclient:async-http-client plus one moby DoS, CVE-2026-107212-adjacent band CVE-2026-107226–107231 / 107280–107285, fixed across 3.0.12 / 3.0.13 / 3.0.14 and 2.16.1; 2.x is EOL — two legs never get a 2.x fix).
One theme: this is the mirror image of the server-side pages — every leg is a bug in an outbound Java HTTP client, and each one moves credentials, cookies, requests, or authenticated sockets across a host, scheme, or identity boundary the application believes is enforced. AsyncHttpClient is embedded across Java enterprise stacks (service integrations, API gateways, crawlers, SDKs), and several legs are "long-standing behaviour, not a recent regression" — vulnerable deployments are everywhere and silent. Several legs also fingerprint their own patch level through behavior, which is directly reusable recon.
Advisory table (async-http-client unless noted)¶
| Leg | GHSA | CVE | Sev | Fixed | Boundary |
|---|---|---|---|---|---|
| Replay-to-different-host sends original host's request + credentials to the new host | GHSA-jmqq-x5g9-9p2w | CVE-2026-107282 | critical | 3.0.13 / 2.16.1 | On request replay (ResponseFilter failover — the documented pattern — or IOException retry), setCurrentRequest runs without setTargetRequest; four consumers read the stale target. Socket to B is pooled under A's key (later A-request served over B's connection with A's Authorization); over a CONNECT tunnel the client TLS-hands-hands with B correctly then writes A's path, A's Host, A's Authorization into B's tunnel — no cert mismatch occurs because the handshake really is with B; A's credentials are regenerated from A's realm onto the replay; and when original was http:// and replay https://, no SSL handler is installed at all — cleartext, credentials included. |
| Digest challenge with unreadable/absent nonce silently downgrades to Basic | GHSA-rqf5-2wxv-rjf4 | CVE-2026-107231 | high | 3.0.13 / 2.16.1 | WWW-Authenticate: Digest realm="x" (no nonce, or nonce="") passes the Digest-scheme check, fails to yield a nonce, and the absence of a nonce selects Basic → Authorization: Basic base64 password on the wire. Reachable by anyone who can write the 401/407: malicious origin, proxy, plaintext-hop attacker. 3.0.12-only extra trigger: realm="C:\" trailing-backslash quoted-pair swallows the closing quote so the nonce is never parsed. |
| Pooled connections shared across NTLM/Negotiate/SPNEGO identities (fix-incomplete follow-up) | GHSA-v2j5-22fr-j62r | CVE-2026-107230 | high | 3.0.14 (3.0.13 only) | The 3.0.13 fix folded the principal into the HTTP/1.1 pool key but left three cases: default Kerberos/SPNEGO deployments leave the realm principal unset → key stays unscoped → a credential-less request to the same host draws the service-identity socket; proxy realm never entered the key; identities sharing a username (CORP\alice vs OTHER\alice) collided, as did two realms differing only in password/keytab. Sharpest target shape named by the advisory itself: a service that calls an internal host under its own Kerberos identity and also fetches user-supplied URLs — your URL on that host is fetched as the service. |
Plaintext HTTP Set-Cookie can plant/overwrite/delete Secure cookies | GHSA-p2jm-6hj6-9rjg | CVE-2026-107226 | med (Integrity: High) | 3.0.14, 2.x never | The store ignores the scheme a Set-Cookie arrived on (6265bis §5.7 steps 13/16 unimplemented); Secure is only checked on retrieval. http://insecure.example.com → Set-Cookie: SID=x; Secure; Domain=example.com ⇒ the next HTTPS request to bank.example.com carries the attacker value inside TLS. Replace and Max-Age=0 delete also work — session fixation into the HTTPS session or CSRF-token overwrite without any network-path position, just a plaintext sibling host. |
Domain= public-suffix check missing — cookie scoped to co.uk | GHSA-f9m8-cv68-674w | CVE-2026-107280 | med | 3.0.13, 2.x never | RFC 6265 §5.3 step 5 (reject Domain that is a public suffix) absent; §5.1.3 suffix-match alone used. attacker.co.uk → Domain=co.uk reaches bank.co.uk. Explicitly the residual of CVE-2026-55688 — the direct unrelated-domain form was fixed and verified as a control; the public-suffix test was simply never added. Fix bundles the PSL as stale-able data. |
Store silently replaces caller's explicit Cookie header (CVE-2024-53990 fix bypass) | GHSA-2jwh-9rmr-j4xf | CVE-2026-107228 | med | 3.0.14, 2.x EOL | setHeader("Cookie", …) is discarded whenever the store holds any cookie for the origin — store cookies are written into the header after it. Sharpest on one shared client serving many users: user A's request goes out with a session cookie the store took from user B's response. The 2024 fix worked on the cookie list; the header path never reached it. |
| WebSocket proxy credentials leak to origin over CONNECT tunnel | GHSA-3wp9-xfwm-rjjf | CVE-2026-107285 | med | 3.0.12 / 2.16.1 | CONNECT tunnelling check includes ws://, but the proxy-auth gate and request-target selection keyed only on "is URI secured" (false for ws://) → tunneled WebSocket upgrade carries Proxy-Authorization + absolute-form target to the origin server. Basic = direct credential read; Digest = replayable/offline-crackable response. Sibling-branch mismatch inside one component again. |
Digest cnonce from ThreadLocalRandom | GHSA-mfj3-87qq-382v | CVE-2026-107283 | low | 3.0.12 / 2.16.1 | RFC 7616 §3.3 unpredictability requirement; NTLM/SCRAM in the same client already used SecureRandom — one scheme missed. Weak-nonce aids chosen-plaintext/precomputation against the credential. |
WebSocket permessage-deflate unbounded inflate bomb | GHSA-x8v2-478q-2hvg | CVE-2026-107227 | high | 3.0.14, 2.x never | Frame aggregator sits in front of the inflater, so webSocketMaxFrameSize/MaxBufferSize bound only compressed bytes; ~2 MiB inflates to ~2 GiB (Netty buffer max), heap exhaustion held by a persistent server. Opt-in compression. Availability-class — listed for the fuzzer map, not published as its own finding. |
| moby/sys/user unbounded passwd/group parse DoS | GHSA-mjcv-p78q-w5fw | CVE-2026-61801 | med | — | Tracked, container-tooling parser DoS. |
Why this is an operator page¶
- Client-side bugs are a hunting ground you can test entirely on your own infrastructure. No customer target is needed to prove any leg here: an owned origin that emits crafted 401s, an owned plaintext HTTP host, an owned listener behind your own CONNECT-tunneling proxy, a lab JVM running the pinned versions. That makes this class ideal for building reusable, report-ready evidence kits before you ever touch scope — and for bounty scopes that include vendor integrations (anything Java that embeds AHC).
- The critical leg gives you a generic credential-crossing probe for any Java service integration. Failover-
ResponseFilterand retry-on-IOException are documented, ordinary usage — meaning many deployments reproduce the replay by design. Target-shape fingerprint: a service that (a) configures client credentials or a proxy, and (b) fetches URLs that a lower-trust user can influence. The advisory names the exact shape for the pool-key leg (Kerberos identity + user-supplied URLs). - Fix-history differentials are a free version fingerprint. Three legs are explicit incomplete-fix residuals (pool key ← GHSA-vvp4-63h8-v5pm; public suffix ← CVE-2026-55688; caller-header ← CVE-2024-53990), and one trigger exists only on 3.0.12 (trailing-backslash realm). When you control the server side of the wire, the client's behavior is a version probe: does a nonceless
Digestchallenge get answered withBasic(≤3.0.12-era or unfixed), doesrealm="C:\"still yield a digest response, does aSet-Cookiefrom your plaintext host survive into the next TLS request? Decision table beats banner grabbing for headless Java backends. - Cookies are two independent policy layers — storage and retrieval — and fixing one doesn't fix the other. Both cookie legs are storage-side failures behind a retrieval-side check (
Securefiltered on send; unrelated-domain rejected on store). Every cookie jar you audit gets both matrices: arrive-scheme × Secure-flag × overlay/delete on store, and domain-scope including public suffixes. - The "scheme flag" is the seam. The ws:// credential leak happened because CONNECT-tunnelling keys on "secured URI" while the proxy-auth gate keys on the same boolean without excluding WebSocket — one boolean consumed by two branches with different sibling handling. Same genealogy as the July 30 URL-policy page's branch-sweep rule: enumerate every consumer of a trust boolean separately; correctness in one branch proves nothing about the next.
Replayable validation boundaries (lab-only)¶
- Replay/credentials leg: lab JVM + AHC ≤3.0.12, two origins you own (A with a fake
Authorizationcredential, B as listener). Drive (i) aResponseFilterfailover A→B and inspect what B receives (expect A's path/Host/auth if vulnerable), (ii) pooled-connection crossover by re-requesting A after a replay and watching which listener serves it, (iii) http→https replay proving the cleartext branch. Fake credentials only; the marker is which listener sees which header. - Digest downgrade: serve
Digest realm="x"with and withoutnonce=""and therealm="C:\"form from an owned origin; capture whether anAuthorization: Basicarrives (base64 of your lab password proves the class). - Cookie legs: owned
insecure.example.com-shaped host on lab DNS settingSID=canary; Secure; Domain=<lab-suffix>over plaintext, then observe the next HTTPS request's cookie jar; separately testDomain=values that are public suffixes (co.uk, your lab.localanalogues). Fixation/overwrite/delete battery, synthetic values. - Pool-identity leg: lab KDC or NTLM test server, two lab principals sharing a username across realms, one client; assert whether an uncredentialed request draws the authenticated socket (server-side identity log is the verdict).
- Never point any of this at third-party origins, real proxies, real IdPs, or production Java services; prove credential crossing to your own listeners only.
Cross-references¶
- Branch-sibling trust-boolean rule and guard-to-caller mapping: July 30 URL-policy page (Actual prefix-boundary and Heimdall guard-coverage follow-ups).
- Adapter/implementation-coverage drift invariant: Oct 1 HTTP-client guard-coverage drift page — this wave is the same rule at the library-internal tier.
- Server-side digest/protocol negotiation genealogy: Sept 15 parser/header-route page.
- Fixed-incomplete advisory chains as recon: same one-canonical-form / fix-differential theme as the Vault ACL pair folded Sept 20.
October 8 17:1xZ follow-up: Pydantic AI cloud-metadata blocklist defeated by an IPv6 zone identifier — third guard iteration on the same URL field (CVE-2026-107289 / GHSA-vmxc-h2x2-jmf3, medium, pydantic-ai >=1.56 <1.107.6 / >=2.0b <2.44.0)¶
Where an app opts a URL into local access (FileUrl(force_download='allow-local') or web_fetch_tool(allow_local_urls=True)), the always-blocked cloud-metadata guard compared IPv6 addresses by Python IPv6Address set membership — and Python folds the %zone identifier into equality/hashing — while the network stack ignores the zone for non-link-local destinations. http://[fd00:ec2::254%251]/ is not in the blocklist as a set member and still connects to IMESA metadata, leaking IAM short-term credentials. This is the third remediation of the same field (after CVE-2026-46678, CVE-2026-48782, themselves follow-ups to CVE-2026-25580).
Durable axes (generalizes this page's fix-differential rule and the April 30 n8n IPv4-mapped-IPv6 leg):
- A blocklist is only as canonical as its comparison function. Every IP/host blocklist gets the spelling battery per class: alternate encodings (IPv4-mapped IPv6, octal/hex forms), scope/zone decorations (
%251,%eth0), Unicode/IDNA spellings, and case variants. The fingerprint to grep in Python: blocklist membership checks onipaddressobjects (zone hashes in), versussocketconnections (zone ignored out). Guard pass ≠ stack agreement. - **After the n*th advisory on one field, assume the n+1*th guard has a new unmodeled spelling. Pydantic AI's
web_fetch/FileUrlfetch boundary has now been remediated four times; on any AI-fetch surface, replay the entire historical bypass corpus for that product on the current version instead of testing only today's spelling. (Same same-product re-sweep rule as the PraisonAI waves.) - Proof discipline: canonical link-local/metadata ranges only in a throwaway cloud lab or via an owned resolver serving the zone-scoped spelling to your listener — never live production metadata endpoints.
Tracked in this wave, no publish¶
- Excelize wave (jw42 unbounded-row-days-loop, r7x5 extLst nil-deref, 2j4c OLE/CFB panic, x2q3 negative-size CFB allocation, wp2g ANCHORARRAY recursive-calc hang, CVE-2026-107212–107216) — spreadsheet-parser memory-safety/DoS class per the Sept 20 media-pipeline precedent; revisit if a leg pairs with a read primitive.
- Pydantic AI fpf4 quadratic title-extraction ReDoS + 4x9p OTel span exception-content leak — DoS/telemetry-hygiene class.
- moby DoS mjcv above.
October 8 22:1xZ follow-up: Mechanize (Ruby) reproduces the redirect credential-crossing on a second transport — meta-refresh had no origin notion at all¶
Two legs published 22:09Z for mechanize (GHSA-c6rp-p8xm-4q9f / CVE-2026-107399 and GHSA-2mwr-xjcg-37j7 / CVE-2026-107715): response_follow_meta_refresh fetched the refresh target with no crossed-origin check whatsoever, so credentials set via Mechanize#request_headers= (API keys, session headers) were re-applied in full to an attacker-chosen origin. The HTTP-redirect sibling leg is the same leak on the redirect path. Entry points for an attacker-controlled refresh/redirect: stored content, an open redirect, or any page the crawl touches.
Operator value: this is the AsyncHttpClient "credential crosses host boundary" theme arriving in the Ruby crawler ecosystem, and it sharpens one rule — enumerate every navigation primitive in an HTTP client/agent separately: 30x redirect, meta-refresh, JS-refresh, form-post-auto-submit, external-entity fetch. Each is its own credential-crossing candidate with its own (often missing) origin gate; the redirect path having a strip-on-cross-origin guard says nothing about the meta-refresh path. For bug hunters running crawler agents (mechanize, Scrapy w/ headers, custom Node/Java crawlers): if you control any fetched page and the agent carries auth headers, meta-refresh to your listener is a header-capture primitive — prove with an owned endpoint, report as credential disclosure. Sweep fingerprint: grep client code for follow_meta_refresh/refresh handlers that reuse the request-header object without a same-origin comparison.
October 14 19:1xZ follow-up: Apprise notification library — the attacker controls the response from a configured destination, not the URL (CVE-2026-59180 / GHSA-856c-92hv-3vxx, fixed 1.11.0)¶
Apprise (the notification sidecar wired into monitoring stacks, CI, and alerting pipelines everywhere) follows 3xx redirects by default and re-attaches the user-configured auth headers and secrets on the redirected request: X-Gotify-Key, Authorization, bearer-in-query, custom +Header= values — across origins. Same shape in ~50 HTTP plugins plus the HTTP attachment and config loaders (apprise/attachment/http.py, apprise/config/http.py), which means even config files fetched over http:// leak. The advisory's own threat framing is the durable part: the attacker does not control the Apprise URL — only the response from a destination the operator already trusts (service takeover, malicious update, or MITM on an unprotected path). One 302 from a compromised Gotify/Mattermost/ntfy-style endpoint harvests the operator's secret for the next hop in the chain.
Operator value beyond the third confirmation of the redirect-credential-crossing family (urllib3 Proxy-Authorization leg cited in the advisory, AsyncHttpClient, Mechanize above):
- Notification/webhook sidecars are credential-bearing egress paths with attacker-influenceable responses. On engagements, enumerate every outbound notification destination a platform is configured with (monitoring alert channels, CI notifiers, incident tools); each is a redirect-following client holding a standing secret. If you own/compromise the destination service (or sit on its network path), a 302 to your listener is a secret-capture primitive against the source platform — prove with an owned redirector + sink listener, report as credential disclosure.
- Config/attachment loaders are the forgotten leg. The Apprise fix had to cover
config/http.pyseparately — a fetched config URL leaks its own auth headers on redirect the same way. When auditing any tool with "load config/payload from URL," check the loader's redirect handling separately from the feature's own send path (navigation-primitive enumeration rule from the Mechanize follow-up, applied to non-navigation fetchers). - Code fingerprint unchanged: HTTP client call sites that pass an auth-bearing
headers=dict withoutallow_redirects=False(or an origin-comparing redirect hook) — greprequests.post(...headers=with no redirect control across notification/plugin codebases.