{"id":"CVE-2026-109829","kind":"vulnerability","aliases":["CVE-2026-109829","GHSA-4w7w-5xrc-9h26"],"related":[],"title":"undici vulnerable to cache poisoning and cross-user disclosure via cross-origin redirects","description":"### Impact\n\nWhen an originless `Agent` composes the redirect and cache interceptors as `compose(redirect(), cache())` with a shared cache (`type: 'shared'`), the cache builds its key from the original request URL before the redirect handler changes the origin. The response produced after a redirect is then stored under the original key, even when the redirect crossed to a different origin.\n\nAn attacker who controls the target of a redirect from a trusted URL can have their response cached under that trusted URL. A later caller requesting the trusted URL receives the attacker's body from the shared cache without either origin being contacted, allowing persistent cache poisoning. The same key confusion also discloses private data: when an authenticated request returns a `private, no-store` redirect to a user-specific resource, the final response is stored under the original URL and can be served to a later unauthenticated caller.\n\nThe cache interceptor was introduced in undici 7.0.0, and all 7.x and 8.x releases before the patched versions are affected. Applications are affected only when an originless `Agent`, a shared `MemoryCacheStore` or `SqliteCacheStore`, and the composition `compose(redirect(), cache())` are all used. Origin-bound `Client` and `Pool` dispatchers, and the reverse composition `compose(cache(), redirect())`, are not affected.\n\n### Patches\n\nUpgrade to `7.30.1` or `8.12.2`. Before storing a response, undici now verifies that the origin that produced it still matches the cache key, and preserves each redirect hop's cache-control policy, so cross-origin redirect responses and responses behind a `no-store` or `private` hop are no longer cached under the original key.\n\n### Workarounds\n\nCompose the interceptors as `compose(cache(), redirect())`, use per-origin dispatchers and cache stores, or disable shared caching on redirect-capable requests.","summary":"When an originless undici Agent composes redirect() before cache() with a shared cache store, a response from a cross-origin redirect target is cached under the original URL. An attacker who controls a redirect target can poison that trusted URL for other callers, and a private no-store redirect response can be served to unauthenticated users.","severity":"critical","cvss":9.1,"epss":0.00149,"exploited":false,"exploited_at":null,"products":[{"name":"undici","fixed":["7.30.1"],"affected":">= 7.0.0, < 7.30.1","ecosystem":"npm"},{"name":"undici","fixed":["8.12.2"],"affected":">= 8.0.0, < 8.12.2","ecosystem":"npm"},{"name":"nodejs undici","fixed":["7.30.1"],"affected":">= 7.0.0, < 7.30.1","ecosystem":"vendor"},{"name":"nodejs undici","fixed":["8.12.2"],"affected":">= 8.0.0, < 8.12.2","ecosystem":"vendor"}],"ecosystems":["npm","vendor"],"tags":["cwe-346","cwe-524","http-clients","node.js","npm","package","undici"],"not_affected_if":["default-config","no-shared-cache","origin-bound-dispatchers","reverse-composition"],"how_to_check":"- Run `npm ls undici` to find the installed undici version and whether it is 7.x below 7.30.1 or 8.x below 8.12.2.\n- Search code for `compose(redirect(), cache())` together with `new Agent(` and `type: 'shared'` on a MemoryCacheStore or SqliteCacheStore.","fix":"- Upgrade undici to `7.30.1` (7.x line) or `8.12.2` (8.x line).\n- Alternatively, reverse the order to `compose(cache(), redirect())`, use per-origin dispatchers and cache stores, or disable shared caching on redirect-capable requests.","status":"open","published_at":"2026-10-15T19:17:21+00:00","first_seen_at":"2026-10-27T19:04:55+00:00","triaged_at":"2026-10-27T19:05:51+00:00","updated_at":"2026-10-27T19:33:18+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-27T19:05:51Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade undici to `7.30.1` (7.x line) or `8.12.2` (8.x line).\n- Alternatively, reverse the order to `compose(cache(), redirect())`, use per-origin dispatchers and cache stores, or disable shared caching on redirect-capable requests.","reach":"internet","title":"undici: shared cache stores redirect responses under the original trusted URL","impact":"medium","summary":"When an originless undici Agent composes redirect() before cache() with a shared cache store, a response from a cross-origin redirect target is cached under the original URL. An attacker who controls a redirect target can poison that trusted URL for other callers, and a private no-store redirect response can be served to unauthenticated users.","category":"package","nicknames":[],"who_cares":["node.js","npm","undici","http-clients"],"confidence":0.8,"needs_auth":"unknown","worst_case":"Attacker content is served from a trusted cached URL to other users","analyst_note":"","how_to_check":"- Run `npm ls undici` to find the installed undici version and whether it is 7.x below 7.30.1 or 8.x below 8.12.2.\n- Search code for `compose(redirect(), cache())` together with `new Agent(` and `type: 'shared'` on a MemoryCacheStore or SqliteCacheStore.","needs_analyst":false,"not_affected_if":["origin-bound-dispatchers","reverse-composition","no-shared-cache","default-config"],"who_is_affected":"Applications are exposed only when an originless Agent, a shared MemoryCacheStore or SqliteCacheStore, and the composition compose(redirect(), cache()) are all in use together. Origin-bound Client and Pool dispatchers and the reverse composition compose(cache(), redirect()) are not affected, so teams that use only those can skip it.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.000724,"input_tokens":3922,"output_tokens":664,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-109829':18A,26A '-2026':17A,25A '-346':34B '-524':36B '4w7w':21A '5xrc':22A '7':262C '7.0.0':259C '7.30.1':316C '8':265C '8.12.2':318C '9h26':23A 'affect':273C,276C,312C 'agent':48B,106C,281C 'allow':205C 'also':213C 'applic':274C 'attack':74B,163C,193C 'authent':219C 'behind':355C 'bodi':195C 'bound':298C 'build':125C 'cach':4A,52B,56B,68B,111C,116C,120C,124C,179C,199C,207C,253C,292C,308C,335C,344C,366C,377C,385C,390C 'cache-control':343C 'caller':87B,186C,251C 'capabl':394C 'chang':137C 'client':39B,299C 'compos':49B,107C,114C,290C,307C,372C,376C 'composit':289C,306C 'confus':212C 'contact':204C 'control':76B,165C,345C 'cross':8A,13A,63B,157C,349C 'cross-origin':12A,62B,348C 'cross-us':7A 'cve':16A,24A 'cwe':33B,35B 'data':216C 'differ':160C 'disabl':388C 'disclos':214C 'disclosur':10A 'dispatch':302C,383C 'either':201C 'even':153C 'final':235C 'ghsa':20A 'ghsa-4w7w-5xrc-9h26':19A 'handler':136C 'hop':341C,362C 'http':38B 'http-client':37B 'impact':102C 'interceptor':112C,254C,374C 'introduc':256C 'key':127C,152C,211C,336C,370C 'later':185C,249C 'longer':365C 'match':333C 'memorycachestor':284C 'no-stor':91B,224C,357C 'node.js':40B 'nodejs':29A,31A 'npm':41B 'origin':14A,64B,71B,130C,139C,151C,161C,202C,241C,297C,328C,350C,369C,382C 'origin-bound':296C 'originless':46B,105C,280C 'packag':42B 'patch':270C,313C 'per':381C 'per-origin':380C 'persist':206C 'poison':5A,81B,208C 'polici':346C 'pool':301C 'preserv':338C 'privat':90B,215C,223C,361C 'produc':142C,330C 'receiv':191C 'redirect':15A,50B,65B,78B,94B,109C,115C,135C,145C,156C,170C,227C,291C,309C,340C,351C,378C,393C 'redirect-cap':392C 'releas':267C 'request':131C,187C,220C,395C 'resourc':233C 'respons':59B,95B,141C,178C,236C,322C,352C,354C 'return':221C 'revers':305C 'serv':98B,246C 'share':55B,119C,122C,198C,283C,389C 'specif':232C 'sqlitecachestor':286C 'still':332C 'store':57B,93B,148C,226C,238C,320C,359C,386C 'target':66B,79B,167C 'trust':83B,173C,182C,189C 'type':121C 'unauthent':100B,250C 'undici':1A,27A,28A,30A,32A,43B,47B,258C,323C 'upgrad':314C 'url':72B,84B,132C,174C,183C,190C,242C 'use':295C,379C 'user':9A,101B,231C 'user-specif':230C 'verifi':325C 'version':271C 'via':11A 'vulner':2A 'without':200C 'workaround':371C 'x':263C,266C","nicknames":[],"mitigations":[],"who_affected":"Applications are exposed only when an originless Agent, a shared MemoryCacheStore or SqliteCacheStore, and the composition compose(redirect(), cache()) are all in use together. Origin-bound Client and Pool dispatchers and the reverse composition compose(cache(), redirect()) are not affected, so teams that use only those can skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-15T19:17:21+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-4w7w-5xrc-9h26","url":"https://github.com/advisories/GHSA-4w7w-5xrc-9h26","severity":"high","published_at":"2026-10-27T19:02:31+00:00","modified_at":"2026-10-27T19:02:32+00:00","first_seen_at":"2026-10-27T19:04:55+00:00","fetched_at":"2026-10-27T21:09:21+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-109829","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-109829","severity":null,"published_at":"2026-10-15T19:17:21+00:00","modified_at":"2026-10-19T20:45:43+00:00","first_seen_at":"2026-10-27T19:33:18+00:00","fetched_at":"2026-10-27T19:33:18+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}