CVE-2026-109855: Denial of Service via Unhandled Stream Error in Node.js Undici HTTP/2 WebSockets

Vulnerability ID: CVE-2026-109855
CVSS Score: 7.5
Published: 2026-10-27

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.

TL;DR

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.

⚠️ Exploit Status: POC

Technical Details

  • 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)

Affected Systems

  • Node.js applications using undici 8.11.0 or 8.11.1
  • undici WebSocket client with HTTP/2 enabled (allowH2: true)
  • undici: >= 8.11.0, < 8.11.2 (Fixed in: 8.11.2)

Code Analysis

Commit: dd28c5c

fix: close rejected HTTP/2 WebSocket handshake streams (#5873)

diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 897f122cd10..79601177198 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -101,13 +101,13 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // The presence of a session property on the socket indicates HTTP2 // HTTP1 if (response.socket?.session == null) { - failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error) + failHandshake(handler, response, 1002, 'Received network error or non-101 status code.', response.error)  return }   // 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) // header list results in null, failure, or the empty byte // sequence, then fail the WebSocket connection. 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 }  @@ -138,7 +138,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // _Fail the WebSocket Connection_. // For H2, no upgrade header is expected. if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { - failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".') + failHandshake(handler, response, 1002, 'Server did not set Upgrade header to "websocket".')  return }  @@ -148,7 +148,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // MUST _Fail the WebSocket Connection_. // For H2, no connection header is expected. if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { - failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".') + failHandshake(handler, response, 1002, 'Server did not set Connection header to "upgrade".')  return }  @@ -165,7 +165,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') const digest = crypto.hash('sha1', keyValue + uid, 'base64') if (secWSAccept !== digest) { - failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.') + failHandshake(handler, response, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')  return } } @@ -184,7 +184,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) extensions = parseExtensions(secExtension)   if (!extensions.has('permessage-deflate')) { - failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.') + failHandshake(handler, response, 1002, 'Sec-WebSocket-Extensions header does not match.')  return } } @@ -205,7 +205,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // the selected subprotocol values in its response for the connection to // be established. if (requestProtocols === null || !requestProtocols.includes(secProtocol)) { - failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.') + failHandshake(handler, response, 1002, 'Protocol was not set in the opening handshake.')  return } } @@ -307,6 +307,16 @@ function closeWebSocketConnection (object, code, reason, validate = false) { } }  +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) +} +  /** * @param {import('./websocket').Handler} handler * @param {number} code 

Exploit Details

  • GitHub: Official regression test fixture (test/fixtures/websocket-h2-rejected-handshake.js) demonstrating the unhandled stream error crash.

Mitigation Strategies

  • Upgrade undici to version 8.11.2 or later to ensure rejected HTTP/2 WebSocket streams are explicitly destroyed.
  • Disable HTTP/2 support (allowH2: false) on undici Agent or Client dispatchers passed to WebSocket instances if upgrading is delayed.
  • Restrict outbound WebSocket connections to trusted endpoints and deploy process supervision to automatically restart crashed Node.js workers.

Remediation Steps:

  1. Inspect package.json and package-lock.json (or equivalent lockfiles) to determine if undici 8.11.0 or 8.11.1 is installed.
  2. Run 'npm install undici@latest' or update the version constraint to '>=8.11.2'.
  3. Verify the installed version using 'npm list undici' and redeploy the application.

References

Read the full report for CVE-2026-109855 on our website for more details including interactive diagrams and full exploit analysis.