Oct 28, 2026·10 min read·2 visits
Node.js undici versions 5.15.0 through 6.29.0, 7.0.0 through 7.30.0, and 8.0.0 through 8.12.1 fail to check socket write backpressure when responding to WebSocket PING control frames. A malicious WebSocket server can flood a connected client with 2-byte PING frames while stalling TCP reads, causing unbounded memory growth in the client's outbound socket buffer and terminating the Node.js process via an out-of-memory error.
An uncontrolled resource consumption vulnerability in the WebSocket client implementation of Node.js undici allows a remote WebSocket server to trigger an out-of-memory (OOM) crash in the client process. The vulnerability stems from the failure of the incoming frame parser (ByteParser) to respect socket write backpressure when automatically emitting PONG responses to incoming PING control frames. By continuously streaming small 2-byte PING frames while refusing to read from the TCP connection, a malicious server forces the client to queue outbound PONG frames in the V8 heap without bound.
undici is the high-performance HTTP/1.1 and HTTP/2 client written from scratch for Node.js. In addition to standard HTTP dispatching, undici provides a specification-compliant WebSocket client implementation that backs the global WebSocket API (globalThis.WebSocket) exposed natively in modern Node.js runtimes. Consequently, vulnerabilities within undici's WebSocket subsystem affect both applications that import the undici package directly and applications relying on the built-in Node.js WebSocket client.
CVE-2026-109932 (also tracked as GitHub Security Advisory GHSA-9fm3-97px-rjf6) is an uncontrolled resource consumption vulnerability (CWE-400) and asymmetric resource amplification flaw (CWE-405) located in undici's WebSocket frame receiver (lib/web/websocket/receiver.js). When a client establishes a WebSocket connection to an untrusted or compromised server, the client parses incoming frames through the ByteParser class, which extends Node.js's stream.Writable abstraction.
Under RFC 6455 (The WebSocket Protocol), an endpoint receiving a PING control frame (opcode 0x9) must respond promptly with a PONG control frame (opcode 0xA). In vulnerable versions of undici, ByteParser automatically constructs and writes these PONG responses directly to the underlying transport socket without checking if the socket's write buffer is experiencing backpressure.
If the remote WebSocket server stops reading data from its TCP receive buffer while simultaneously flooding the client with minimal 2-byte PING frames, the client's outbound socket write queue grows without limit. Because ByteParser does not pause incoming frame ingestion while awaiting the socket 'drain' event, the V8 heap and Node.js internal stream buffers accumulate millions of pending write requests until the Node.js process terminates with an out-of-memory (OOM) error.
The root cause of CVE-2026-109932 resides in how ByteParser coordinates state transitions between its readable input loop and the writable state of the underlying net.Socket or tls.TLSSocket instance. In Node.js stream architectures, writing data to a duplex socket via socket.write(chunk) sends data to the operating system's kernel TCP send buffer. If the remote peer stops acknowledging packets or advertises a TCP ZeroWindow, the kernel send buffer fills up. Once the queued data exceeds the stream's highWaterMark, socket.write() returns false and sets the boolean property socket.writableNeedDrain to true, signaling that the caller must cease writing until the 'drain' event is emitted.
In lib/web/websocket/receiver.js, ByteParser processes incoming chunks inside its run(callback) method, which executes a while (this.#loop) iteration. When a frame header and payload are fully read, ByteParser checks whether the opcode represents a control frame (PING, PONG, or CLOSE) using isControlFrame(this.#info.opcode). If the frame is a control frame, it invokes this.parseControlFrame(body) and resets this.#state = parserStates.INFO.
Inside parseControlFrame(body), receiving a PING frame triggers the creation of an outbound WebsocketFrameSend instance containing the PONG response, which is immediately written to this.#handler.socket. However, neither parseControlFrame() nor the caller loop in run() checks the return value of socket.write() or inspects this.#handler.socket.writableNeedDrain. Instead, this.#loop remains true, and ByteParser immediately moves to the next iteration to parse the subsequent PING frame from the inbound buffer.
This creates a severe resource asymmetry (CWE-405). On the wire, an unmasked server-to-client PING frame with an empty payload occupies exactly 2 octets: 0x89 (FIN bit set, opcode 0x9) followed by 0x00 (Mask bit unset, payload length 0). For every 2 bytes transmitted by the server, the client allocates a new masked client-to-server PONG frame buffer (at least 6 bytes on the wire due to the mandatory 4-byte masking key), along with V8 heap objects, Buffer slice descriptors, and linked-list queue nodes inside socket._writableState.buffered. Because input parsing is never suspended, the client converts a low-bandwidth inbound stream into unbounded memory allocation.
The vulnerability is located in lib/web/websocket/receiver.js within the ByteParser class. Prior to the patch, the control frame handling branch inside ByteParser.prototype.run executed parseControlFrame and immediately continued the while (this.#loop) parsing cycle regardless of the transport socket's outbound buffer state.
// Vulnerable code in lib/web/websocket/receiver.js
if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO
// BUG: No check on this.#handler.socket.writableNeedDrain.
// The while(this.#loop) loop continues immediately, parsing more PINGs
// and queuing infinite PONGs even when the socket cannot flush data.
} else {
if (!this.#info.compressed) {
if (!this.writeFragments(body)) {
// Data frames handled flow control, but control frames did not.In commits b217373bb024cd93f85eb5b3ffd25684855f60f8, 39f93e142df03e559fe7c3d1fc7fba9beee2c185, and 53944624462b6797560288c55dbd8c66857bb495, the maintainers patched ByteParser by inspecting this.#handler.socket.writableNeedDrain immediately after this.parseControlFrame(body) returns.
File: lib/web/websocket/receiver.js
@@ -252,6 +252,19 @@ class ByteParser extends Writable {
if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO
+
+ // If the automatic PONG/CLOSE reply filled the socket's write buffer,
+ // stop parsing and resume on drain so a PING flood cannot grow the
+ // outbound queue without bound.
+ if (this.#loop && this.#handler.socket.writableNeedDrain) {
+ this.#loop = false
+ this.#handler.socket.once('drain', () => {
+ if (this.destroyed) return
+ this.#loop = true
+ this.run(callback)
+ })
+ return
+ }
} else {
if (!this.#info.compressed) {
if (!this.writeFragments(body)) {The fix introduces three critical synchronization steps when this.#loop is true and this.#handler.socket.writableNeedDrain evaluates to true:
this.#loop = false and returns early from run(callback) without invoking callback(). Because run is invoked by the Writable._write(chunk, encoding, callback) method of ByteParser, withholding the completion callback propagates backpressure upstream to the readable side of the socket, halting further reads from the OS network stack.this.#handler.socket.once('drain', ...) to wait until the kernel TCP send buffer flushes and the outbound queue drains below its highWaterMark.'drain' fires, the callback verifies that the parser has not been destroyed (if (this.destroyed) return), sets this.#loop = true, and re-invokes this.run(callback) to process any remaining bytes in the current chunk before finally signaling readiness for more inbound data.> [!NOTE]
> Fix Completeness Assessment: The patch checks writableNeedDrain for all control frames (PING, PONG, and CLOSE) processed by parseControlFrame(body). Because CLOSE frames only trigger a single reciprocal CLOSE response before terminating the state machine (this.#loop becomes false), and PONG frames do not generate automatic replies, halting on writableNeedDrain when this.#loop is true effectively covers the PING flood vector without introducing listener leaks on connection teardown.
Exploiting CVE-2026-109932 requires a target Node.js application using an affected version of undici (or Node.js's built-in globalThis.WebSocket) to initiate a WebSocket connection to an attacker-controlled or compromised server endpoint. This scenario commonly occurs in server-side applications that connect to external WebSocket feeds (such as cryptocurrency market tickers, webhook relays, federated messaging protocols, or remote telemetry endpoints) or applications that accept user-supplied WebSocket URLs.
Once the client initiates the HTTP Upgrade: websocket handshake, the malicious server completes the RFC 6455 handshake by returning an HTTP/1.1 101 Switching Protocols response with the computed Sec-WebSocket-Accept header. At this point, the target Node.js process instantiates ByteParser and pipes the underlying TCP/TLS socket into it.
To trigger the memory exhaustion condition, the server executes a two-part asymmetric attack:
recv() or read() on the accepted socket descriptor (or sets SO_RCVBUF to a minimal value and pauses reading). Once the client transmits enough PONG data to fill the server's OS TCP receive window and the client's local OS TCP send buffer, the client socket transitions to writableNeedDrain = true.0x89 0x00 byte pairs over the socket.// Conceptual demonstration based on test/websocket/pong-backpressure.js
// A single unmasked server-to-client PING frame with 0-length payload:
const serverPingFrame = Buffer.from([0x89, 0x00])
// Concatenating thousands of 2-byte PING frames into a single TCP payload:
const payload = Buffer.concat(Array.from({ length: 10000 }, () => serverPingFrame))As documented in the regression test test/websocket/pong-backpressure.js included in the patch commit, when a blocked socket (writableNeedDrain === true) receives a batch of 50 consecutive PING frames (100 bytes total), a patched client halts after writing exactly 1 PONG frame (socket.pongWrites === 1) and pauses parsing until socket.drain() is emitted. In contrast, an unpatched client parses all 50 frames immediately and queues 50 outbound writes. Over a sustained network stream, sending megabytes of packed 0x89 0x00 sequences causes the unpatched client to allocate gigabytes of heap memory in seconds, crashing the Node.js runtime with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory.
The primary impact of CVE-2026-109932 is a complete Denial of Service (DoS) of the Node.js application process via memory exhaustion. Because Node.js executes JavaScript on a single-threaded event loop within a shared V8 isolate by default, exhausting the V8 heap or system RSS causes the entire process to abort immediately. Any concurrent HTTP requests, background jobs, or unrelated WebSocket connections handled by that same Node.js process are terminated abruptly.
The vulnerability carries a CVSS v3.1 base score of 5.9 (Medium) with the vector CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H:
C:N) and Integrity (I:N) remain unaffected.From a threat intelligence perspective, the Exploit Prediction Scoring System (EPSS) assigns CVE-2026-109932 a score of 0.00321 (0.32%), placing it in the 24.94th percentile. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and no public weaponized exploit modules currently exist in ExploitDB or Metasploit.
To remediate CVE-2026-109932, organizations must upgrade the undici package to a patched release that enforces socket drain backpressure during WebSocket control frame processing. The OpenJS Foundation and undici maintainers have released fixes across all supported major branches:
6.29.1 or later (note that 5.x users must upgrade to the 6.x release line starting at 6.29.1).7.30.1 or later.8.12.2 or later.For applications that do not declare undici in package.json directly but instead utilize the native globalThis.WebSocket client built into the Node.js runtime, administrators must update the Node.js runtime binary to a release that bundles a patched version of undici (>= 6.29.1, >= 7.30.1, or >= 8.12.2).
> [!WARNING]
> There are no configuration-based workarounds within undici to disable automatic PONG responses or enforce control frame backpressure on unpatched versions. Until patches are applied, applications should restrict outbound WebSocket connections Strictly to trusted, verified endpoints and avoid connecting to arbitrary user-supplied ws:// or wss:// URIs.
Security and operations teams can verify whether vulnerable versions of undici are present in their dependency trees using standard package manager inspection commands:
# Inspect installed undici versions across the dependency tree
npm ls undici
# Run automated vulnerability auditing
npm auditIn production environments, defenders should also configure process-level memory telemetry (monitoring process.memoryUsage().heapUsed and socket.writableLength on active WebSocket clients) and enforce outbound egress filtering so backend services cannot establish WebSocket sessions to untrusted external hosts.
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 / OpenJS Foundation | >= 5.15.0, <= 6.29.0 | 6.29.1 |
undici Node.js / OpenJS Foundation | >= 7.0.0, <= 7.30.0 | 7.30.1 |
undici Node.js / OpenJS Foundation | >= 8.0.0, <= 8.12.1 | 8.12.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) / CWE-405 (Asymmetric Resource Consumption) |
| CVSS v3.1 Score | 5.9 Medium (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| Affected Component | undici WebSocket Client (ByteParser in lib/web/websocket/receiver.js) |
| Attack Vector | Network (Malicious WebSocket server flooding 2-byte PING frames while stalling TCP reads) |
| Primary Impact | Denial of Service (Out-of-Memory crash of the Node.js runtime process) |
| Patched Versions | undici 6.29.1, 7.30.1, 8.12.2 |
| EPSS Score | 0.32% (24.94th percentile) |
| CISA KEV Status | Not Listed |
The software does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
The Model Context Protocol (MCP) TypeScript SDK contains an uncontrolled resource consumption vulnerability in its HTTP server transport implementations and middleware adapters. Affected versions fail to enforce upper bounds on incoming HTTP POST request body sizes and JSON-RPC 2.0 batch array lengths. In addition, middleware wrappers for Express and Hono parse request bodies into memory prior to validating Host and Origin headers. Unauthenticated remote attackers can exploit these flaws to trigger Out-Of-Memory (OOM) process crashes or monopolize the single-threaded Node.js event loop.
An uncaught exception vulnerability (CWE-248) exists in the Node.js HTTP/1.1 client library undici, affecting versions 7.15.0 through 7.30.0 and 8.0.0 through 8.12.1. The flaw resides in the DecompressHandler interceptor (lib/interceptor/decompress.js), which processes compressed HTTP response streams using asynchronous Node.js zlib callbacks. When forwarding decompressed data chunks or stream completion events to downstream handlers, the interceptor fails to wrap the invocations of super.onResponseData() and super.onResponseEnd() in try...catch blocks. If a downstream interceptor or handler throws an exception during response processing—such as the built-in dump interceptor enforcing a maximum response size—the exception escapes the asynchronous zlib callback stack onto the top-level Node.js event loop, terminating the entire client process.
CVE-2026-87018 (GHSA-qrxm-fqhp-jwvm) is a high-severity Denial of Service (DoS) vulnerability in undici, the default HTTP/1.1 and HTTP/2 client for Node.js that backs the global fetch() implementation. The flaw stems from reachable assertions in undici's socket and session error handlers, which assume that TLS hostname verification errors (ERR_TLS_CERT_ALTNAME_INVALID) cannot occur after the initial connection establishment. By initiating a post-handshake TLS 1.2 renegotiation and presenting a certificate with a mismatched Subject Alternative Name (SAN), a remote HTTPS server can trigger this assertion and terminate the entire Node.js runtime process via an uncaught AssertionError.
FileBrowser Quantum prior to versions 1.5.2-stable and 2.0.1-beta contains an improper link resolution vulnerability (CWE-59) in its file indexing and retrieval subsystems. When processing file read, download, preview, media streaming, OnlyOffice integration, or public share requests, the application resolves symbolic links via Go's filepath.EvalSymlinks without verifying that the canonical target path remains within the configured source root and the user's assigned access scope. Consequently, authenticated users with read access or unauthenticated holders of public share links can read arbitrary server-accessible files outside their designated directory boundaries.
CVE-2026-116676 (GHSA-rjh2-4xgw-p2hj) is an OS command injection vulnerability (CWE-78) in Atlantis, an open-source Go application that automates Terraform pull request workflows via webhooks. From version 0.4.0 prior to version 0.47.1, Atlantis constructed Terraform and Conftest commands by joining argument slices into a single space-separated string and passing them to a shell interpreter (`sh -c`). Because user-controlled workspace names from `atlantis.yaml`, pull request comments, HCL configurations, and API requests were insufficiently validated against shell metacharacters, an authenticated contributor eligible to trigger a Terraform plan can execute arbitrary commands under the Atlantis service account.
CVE-2026-116679 (GHSA-5cp7-fp4j-wwcv) is an incorrect authorization vulnerability (CWE-863) in m1k1o/neko, a self-hosted virtual browser running inside Docker via WebRTC. In versions from 3.0.0 prior to 3.1.5, a boolean logic inversion in the LegacyIsHost() method within server/internal/session/session.go causes the server to unconditionally grant host input privileges over the legacy WebRTC data channel when implicit hosting is enabled. Authenticated non-privileged room members can exploit this flaw to bypass room control locks and profile restrictions, allowing unauthorized injection of mouse and keyboard events into the shared virtual browser session.