Sep 29, 2026·8 min read·5 visits
An unmitigated resource leak in undici's RetryHandler causes application-level hangs and Denial of Service when handling aborted responses followed by non-retryable errors.
A resource management vulnerability in the Undici HTTP client (CWE-772) occurs when the retry interceptor receives a partial body payload followed by a non-retryable response error on a subsequent connection attempt, resulting in orphaned streams and potential Denial of Service (DoS).
Undici is a high-performance HTTP/1.1 and HTTP/2 client for Node.js. Applications rely on its robust connection pooling and automatic retry handling capabilities to maintain network resilience. The vulnerability designated as CVE-2026-18149 targets the core logic of Undici's retry interceptor, specifically how it manages the lifecycle of stream resources when connection errors interact with subsequent HTTP protocol exceptions.
The vulnerability is formally categorized under CWE-772: Missing Release of Resource after Effective Lifetime. It occurs when a client application initiates a request, and Undici's automatic retry mechanism fails to settle or terminate a previously exposed, truncated response body stream. This state desynchronization occurs if a subsequent retry fails with a non-retryable HTTP status code, leaving the initial body stream permanently orphaned.
From an operational perspective, the downstream client remains in a perpetual waiting state, blocking concurrent connections and eventually causing resource exhaustion. Because the handler substitutes the connection's active parser without destroying the original stream, the standard timeout rules configured at the body level fail to execute. This allows an attacker with control of a remote target server to systematically crash or freeze a client application with minimal bandwidth expenditure.
To trace the root cause, it is necessary to examine the interaction between Undici's connection parser and its middleware handler layer. When an HTTP server issues a 200 OK response with a declared Content-Length header, Undici receives the headers and executes onResponseStart(). This action successfully propagates the headers downstream and creates a readable body stream that is delivered to the client application.
If the server terminates the socket connection before transmitting the full body payload, the HTTP parser registers an premature-end-of-stream error. Because the request is eligible for retry, Undici's internal retry engine attempts to re-establish a connection and replay the request. At this juncture, the downstream application code is already waiting on the first stream's response body, holding active Promise callbacks that expect either complete data or an explicit error signal.
The flaw materializes when the retried request completes with a non-retryable HTTP status, such as a 400 Bad Request. When processing this second response, the unpatched RetryHandler immediately updates its internal state tracking, assuming it must present this new response to the client. It completely replaces the stream controller context for the downstream application without executing any termination or error handling routines on the original response stream, leaving the initial body permanently suspended in memory.
A critical exacerbating factor is that standard socket body-timeouts are bound directly to the active connection parser. Once the parser context is reassigned to the new, terminated retry socket, the orphaned body stream no longer has an active timer monitoring its lifecycle. The client application remains blocked on the unresolved stream, and the Node.js event loop retains the associated callbacks, preventing garbage collection of the stream objects and TCP structures.
The vulnerability is mitigated by enforcing explicit state checks during response lifecycle transitions within the lib/handler/retry-handler.js file. The patch targets the critical onResponseStart function and the internal shouldRetry error processing pathway. By introducing a guard clause that checks the status of this.headersSent, the handler is prevented from silently overriding the stream context when a response has already been initiated downstream.
Below is the technical diff showing the logical correction applied across the vulnerable handlers in undici:
// File: lib/handler/retry-handler.js (Vulnerable vs. Patched)
// Vulnerable Implementation:
onResponseStart(controller, statusCode, headers, statusMessage) {
if (this.retryOpts.throwOnError) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.checkpointResponseEnd(headers)
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
}
}
}
// Patched Implementation:
onResponseStart(controller, statusCode, headers, statusMessage) {
if (this.retryOpts.throwOnError) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
if (this.headersSent) {
// The downstream handler already received the response from an
// earlier attempt. Forwarding this response would replace the
// downstream body and leave the original body pending forever.
this.handler.onResponseError?.(this.controllerProxy, err)
} else {
this.headersSent = true
this.checkpointResponseEnd(headers)
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
}
}
}
}When evaluating the patched logic, the integration of this.headersSent as a conditional guard is crucial. If the headers were already transmitted during an earlier, partially successful attempt, forwarding the new error response is skipped. Instead, the library invokes onResponseError with a designated error object, which triggers proper stream cleanup.
This callback execution propagates an error (typically UND_ERR_REQ_RETRY) directly into the active stream controller. The stream is terminated with this exception, resolving any pending Promises such as those returned by body.text(), body.json(), or manual stream read loops. This ensures that the application code receives an actionable error instead of hanging indefinitely, thereby releasing the associated system resources and allowing the event loop to function normally.
Exploitation of this vulnerability requires an attacker to control a server that is targeted by an Undici-based client. Alternatively, a malicious actor could intercept traffic via a Man-in-the-Middle position if connection encryption is absent or misconfigured. The attacker does not need to execute any binary payloads or construct highly complex shellcodes; instead, they manipulate the state machine of the HTTP client.
To successfully trigger the vulnerability, the attacker's server must orchestrate two distinct response stages. First, the server must accept an incoming connection, return a standard success header (such as 200 OK) specifying a non-zero Content-Length, send only a portion of the body, and immediately tear down the socket. Second, when the client attempts to execute its configured automatic retry, the server must respond with an HTTP status code that is deemed non-retryable by the client, such as 400 Bad Request.
Below is a state diagram representing the lifecycle of the vulnerability under an attack scenario:
The security impact accumulates rapidly with repeated client requests. Every subsequent request directed to the malicious endpoint spawns an additional hung Promise and an unclosed socket stream handler. Because these streams are not tracked by active connection timeouts, they persist until the Node.js application process runs out of available heap space or file descriptors, culminating in a total denial of service.
The primary operational consequence of CVE-2026-18149 is application-level Denial of Service. In enterprise environments where microservices communicate continuously via HTTP APIs, a single compromised or malicious upstream dependency can render entire calling systems unresponsive. Because Node.js operates on a single-threaded event loop, the rapid accumulation of unresolved promises and open file descriptors degrades the event loop's overall execution speed and concurrency handling capabilities.
The vulnerability has been assigned a CVSS score of 5.9 (Medium) with the vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H. The medium-level rating reflects the high complexity of the attack, which relies on a specific sequence of network behaviors and timing. However, the impact on availability is rated as High, given that a simple sequence of two short responses can completely incapacitate the client process.
According to current threat intelligence, the Exploit Prediction Scoring System (EPSS) score stands at approximately 0.0036, indicating a low immediate probability of widespread automation-based exploitation. Furthermore, this vulnerability is not currently listed in the CISA Known Exploited Vulnerabilities catalog. However, the lack of active exploitation should not discourage immediate remediation, as the attack vectors are highly reliable and easily replicated by targeting known microservice communication pathways.
Complete remediation of this vulnerability requires upgrading the undici package to safe versions. Development teams should audit their project dependency trees and ensure that any references to undici are updated to either 7.29.1 (for 7.x deployments) or 8.10.2 (for 8.x deployments). These versions contain the definitive state-checks that ensure all orphaned stream handlers are cleanly rejected and closed when error states occur during retries.
If an immediate package upgrade is not feasible due to strict legacy dependencies or release freeze policies, several mitigation strategies can reduce the risk. Security engineers should evaluate the use of the retry interceptor. If the client is fetching resources from untrusted external sources, disabling automatic retry logic or limiting the retry scope strictly to safe, idempotent endpoints will neutralize the attack vector.
In addition, applications should adopt defensive coding patterns by wrapping all network body parsing calls in strict timeout blocks. Rather than relying exclusively on Undici's internal connection or body timeout parameters, developers should employ standard JavaScript mechanisms such as AbortSignal.timeout() or custom Promise.race() patterns. This ensures that even if an underlying library stream fails to settle, the application execution path is forcibly aborted and cleaned up after a deterministic period.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
undici Node.js | >= 7.11.0 < 7.29.1 | 7.29.1 |
undici Node.js | >= 8.0.0 < 8.10.2 | 8.10.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-772 |
| Attack Vector | Network (AV:N) |
| CVSS Score | 5.9 (Medium) |
| EPSS Score | 0.0036 (0.36%) |
| Impact | Denial of Service (DoS) |
| Exploit Status | Proof-of-Concept (PoC) available |
| KEV Status | Not listed |
The software does not release a resource after its effective lifetime has ended, which can lead to resource exhaustion.
CVE-2026-101894 is a critical path traversal vulnerability in @xhmikosr/decompress before versions 10.2.2 and 11.1.4, stemming from an incomplete hardening bypass of CVE-2026-53486 where static lexical containment checks fail to detect kernel-level resolution of crafted symlink chains, allowing arbitrary local file modification and execution.
A security-critical desynchronization vulnerability exists in fast-uri versions 4.1.3 and 4.1.4. Due to incorrect order-of-operations, the mailto scheme parser validates raw percent-encoded parameter keys instead of normalized keys, but subsequently decodes and writes them into a generic headers object. When the parsed URI is serialized, these keys are re-emitted literally, allowing attackers to bypass validation boundaries and smuggle unauthorized recipients, subjects, or body parameters in downstream mailing applications.
CVE-2026-86472 is a validation bypass vulnerability in fast-uri (a high-performance RFC 3986 URI toolbox heavily used by popular Node.js frameworks like Fastify and validation libraries like AJV). The vulnerability stems from improper handling of case sensitivity (CWE-178) due to an incorrect order of operations during hostname canonicalization in scheme-relative URLs. An attacker can leverage percent-encoded uppercase characters within scheme-relative URLs to bypass domain blocklists/allowlists in downstream applications. Because hostname resolution in DNS and HTTP is case-insensitive, the bypassed host representation still routes to the target destination, resulting in potential Server-Side Request Forgery (SSRF) or security control bypasses.
An unauthenticated remote attacker can crash NestJS microservices utilizing TCP or RabbitMQ transport layers. The vulnerability exists due to recursive serialization of deeply nested message patterns using JSON.stringify, leading to a RangeError and process termination.
A vulnerability in PyJWT's JWK Set parsing logic allows a malformed RSA key to trigger an unhandled ValueError, leading to an application-wide or request-level Denial of Service.
An uncontrolled recursion vulnerability exists in Nodemailer versions up to and including 10.0.1. When parsing recipient email addresses, recursively nested arrays bypass the parser's depth limit, resulting in V8 call stack exhaustion and immediate synchronous process termination.