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:
- Inspect package.json and package-lock.json (or equivalent lockfiles) to determine if undici 8.11.0 or 8.11.1 is installed.
- Run 'npm install undici@latest' or update the version constraint to '>=8.11.2'.
- Verify the installed version using 'npm list undici' and redeploy the application.
References
- GitHub Security Advisory: GHSA-qq8j-7v2v-3rwp
- Fix Commit: close rejected HTTP/2 WebSocket handshake streams
- Pull Request #5873
- Undici v8.11.2 Release Notes
- NVD - CVE-2026-109855
- CVE.org Record: CVE-2026-109855
- OpenJS Foundation Security Advisories
Read the full report for CVE-2026-109855 on our website for more details including interactive diagrams and full exploit analysis.

