Oct 28, 2026·11 min read·1 visit
An invalid assumption in undici's HTTP/1.1 and HTTP/2 socket error listeners allows a remote TLS 1.2 server to crash the client Node.js process by initiating post-handshake renegotiation with a hostname-mismatched certificate.
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.
Undici is the official HTTP/1.1 and HTTP/2 client maintained by the OpenJS Foundation and bundled directly into the Node.js core runtime to provide the global fetch() API. Because it serves as the foundational networking layer for outbound HTTP requests in modern Node.js applications, stability within its socket management and error handling routines is critical to process availability. When a Node.js application communicates with external web services, webhooks, or third-party APIs, undici manages the underlying TCP and TLS socket lifecycles across connection pools.
CVE-2026-87018 is classified under CWE-617 (Reachable Assertion) and CWE-248 (Uncaught Exception). The vulnerability exists in the error event listeners attached to established HTTP/1.1 sockets (lib/dispatcher/client-h1.js) and HTTP/2 sessions and sockets (lib/dispatcher/client-h2.js). Specifically, the error handlers explicitly assert that the Node.js TLS error code ERR_TLS_CERT_ALTNAME_INVALID cannot be emitted once a connection has transitioned past the initial handshake state.
While certificate identity verification typically occurs exclusively during the initial TLS handshake, the TLS 1.2 protocol specification permits post-handshake session renegotiation. If an outbound client connects to a server over TLS 1.2, the remote peer can complete the initial handshake with a valid certificate and subsequently initiate a renegotiation handshake presenting a different certificate. When the Node.js TLS subsystem validates the new certificate and detects a hostname mismatch, it emits ERR_TLS_CERT_ALTNAME_INVALID on the active socket.
When undici intercepts this error event on an active connection, the hardcoded assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') statement evaluates to false. Because this assertion executes asynchronously inside a Node.js EventEmitter callback without an enclosing try/catch block, the resulting AssertionError propagates to the top of the event loop as an uncaught exception. Consequently, a single outbound request to an untrusted or compromised HTTPS endpoint terminates the entire Node.js process.
The root cause of CVE-2026-87018 is a violation of state invariants between the Node.js node:tls subsystem and undici's dispatcher state machine. When undici establishes an outbound HTTPS connection, the initial TLS handshake validates the server's X.509 certificate chain and verifies that the target hostname matches the certificate's Subject Alternative Name (SAN) or Common Name (CN). If this check fails during the initial handshake, the connection is aborted before undici attaches its post-connection socket listeners.
Based on the assumption that hostname verification is strictly an initial connection phase operation, undici developers placed a defensive assertion inside onHttpSocketError, onHttp2SessionError, and onHttp2SocketError. These functions are bound to the error events of sockets and HTTP/2 sessions only after the connect event has succeeded. The assertion assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') was intended to verify internal state consistency, assuming that any ERR_TLS_CERT_ALTNAME_INVALID error would have been caught by the pre-connection handshake handler.
This assumption fails under TLS 1.2 (RFC 5246), which supports post-handshake renegotiation. At any point after the initial handshake completes and application data begins flowing, a TLS 1.2 server can transmit a HelloRequest message to initiate a new handshake over the existing encrypted channel. During this secondary handshake, the server can swap its cryptographic credentials (for example, by invoking socket.setKeyCert() in a Node.js TLS server) and present an alternative X.509 certificate.
When the client-side OpenSSL and Node.js tls.TLSSocket implementation process the renegotiated handshake, Node.js automatically re-runs certificate verification and hostname validation against the original Server Name Indication (SNI) hostname. If the newly presented certificate is signed by a trusted Certificate Authority (CA) so that it passes cryptographic chain validation, but its SAN/CN does not match the target hostname, node:tls destroys the socket and emits an error event with err.code = 'ERR_TLS_CERT_ALTNAME_INVALID'.
Because the socket is already in an established state within undici's connection pool, the error is routed directly to onHttpSocketError (for HTTP/1.1) or onHttp2SessionError / onHttp2SocketError (for HTTP/2). Instead of passing the error object down the standard promise rejection pipeline to fail the in-flight HTTP request, the execution hits assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') and immediately aborts the runtime.
The vulnerability spans two core dispatcher files in the undici repository: lib/dispatcher/client-h1.js (handling HTTP/1.1 connections) and lib/dispatcher/client-h2.js (handling HTTP/2 multiplexed sessions). In both modules, socket error handlers are attached to manage network failures such as ECONNRESET, ETIMEDOUT, or TLS alert frames that occur during active request-response cycles.
In the vulnerable versions (>= 4.2.1, < 6.29.1, >= 7.0.0, < 7.30.1, and >= 8.0.0, < 8.12.2), the first line of execution in three separate error handling functions was a strict assertion checking the error code. Commit e257e9f8fb42eedd50727b9cf65e548c8925e4a8 resolves the issue by removing these assertions entirely.
diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js
index c8d8ecb..9e71bf0 100644
--- a/lib/dispatcher/client-h1.js
+++ b/lib/dispatcher/client-h1.js
@@ -988,8 +988,6 @@ function connectH1 (client, socket) {
}
function onHttpSocketError (err) {
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
-
const parser = this[kParser]
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js
index 72bc64a..9a1c0d4 100644
--- a/lib/dispatcher/client-h2.js
+++ b/lib/dispatcher/client-h2.js
@@ -600,8 +600,6 @@ function onHttp2SendPing (session) {
}
function onHttp2SessionError (err) {
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
-
this[kSocket][kError] = err
if (this[kReceivedGoAway]) {
@@ -767,8 +765,6 @@ function onHttp2SocketClose () {
}
function onHttp2SocketError (err) {
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
-
this[kError] = err
if (this[kHTTP2Session]?.[kReceivedGoAway]) {Analyzing the code path in lib/dispatcher/client-h1.js after the removal of the assertion demonstrates why no additional error-handling logic was required. Once the assert() statement is removed from onHttpSocketError(err), the function assigns this[kError] = err on the socket instance and proceeds to clean up the HTTP parser state. When the socket subsequently emits its close event, undici's close handler reads this[kError] and invokes the internal error callback for all pending requests queued on that client, rejecting their promises with the ERR_TLS_CERT_ALTNAME_INVALID error.
Similarly, in lib/dispatcher/client-h2.js, removing the assertions in onHttp2SessionError and onHttp2SocketError allows this[kSocket][kError] and this[kError] to be populated with the TLS error object. The HTTP/2 dispatcher then tears down the session and propagates the error to all active HTTP/2 streams associated with that connection.
> [!NOTE]
> Fix Completeness Assessment: The patch completely eliminates the reachable assertion across both HTTP/1.1 and HTTP/2 implementations. Because no other assertions in onHttpSocketError, onHttp2SessionError, or onHttp2SocketError inspect specific TLS error codes, variant attacks utilizing other post-handshake TLS error codes (such as ERR_TLS_CERT_EXPIRED or ERR_TLS_HANDSHAKE_TIMEOUT) will likewise be handled as standard socket errors rather than triggering assertion failures.
Exploiting CVE-2026-87018 requires the attacker to induce the target Node.js application to make an outbound HTTPS request to an attacker-controlled server. Common application features that expose this attack surface include webhook delivery systems, federated protocol implementations (such as ActivityPub), web scrapers, URL preview generators, OAuth/OIDC discovery endpoints, and Server-Side Request Forgery (SSRF) vectors.
Two technical prerequisites must be satisfied for exploitation to succeed:
TLSv1.2. TLS 1.3 (RFC 8446) removed support for renegotiation from the protocol entirely. However, an attacker controlling the destination HTTPS server dictates the negotiated protocol version during the ServerHello phase. As long as the Node.js client allows TLS 1.2 (which is enabled by default in Node.js alongside TLS 1.3), the server can force the connection to downgrade to TLSv1.2.attacker.example.com) and a second valid certificate issued for any other domain (e.g., other.example.org). If the second certificate is self-signed or untrusted, Node.js will fail with UNABLE_TO_VERIFY_LEAF_SIGNATURE or DEPTH_ZERO_SELF_SIGNED_CERT prior to checking the hostname, which would bypass the specific ERR_TLS_CERT_ALTNAME_INVALID assertion.The official regression test introduced in test/tls-renegotiation.js within commit e257e9f8fb42eedd50727b9cf65e548c8925e4a8 demonstrates the exact exploitation sequence across both http/1.1 and h2 ALPN protocols:
'use strict'
const assert = require('node:assert/strict')
const { constants } = require('node:crypto')
const { once } = require('node:events')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const { createServer } = require('node:tls')
const { test } = require('node:test')
const { Client } = require('undici')
const matching = {
key: readFileSync(join(__dirname, 'fixtures', 'tls-renegotiation', 'matching-key.pem')),
cert: readFileSync(join(__dirname, 'fixtures', 'tls-renegotiation', 'matching-cert.pem'))
}
const wrongHost = {
key: readFileSync(join(__dirname, 'fixtures', 'tls-renegotiation', 'wrong-key.pem')),
cert: readFileSync(join(__dirname, 'fixtures', 'tls-renegotiation', 'wrong-cert.pem'))
}
const ca = readFileSync(join(__dirname, 'fixtures', 'tls-renegotiation', 'ca.pem'))
for (const protocol of ['http/1.1', 'h2']) {
test(`TLS certificate errors after ${protocol} connection establishment reject the request`, async (t) => {
let startRenegotiation
const renegotiationStarted = new Promise((resolve) => {
startRenegotiation = resolve
})
// Configure server to force TLS 1.2 and disable session resumption on renegotiation
const server = createServer({
...matching,
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2',
secureOptions: constants.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION,
ALPNProtocols: [protocol]
}, (socket) => {
socket.on('error', () => {})
socket.once('data', () => {
setTimeout(() => {
// Swap certificate to mismatched hostname after initial data arrives and renegotiate
socket.setKeyCert(wrongHost)
startRenegotiation(socket.renegotiate({
requestCert: false,
rejectUnauthorized: false
}, () => {}))
}, 50)
})
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
allowH2: protocol === 'h2',
connect: {
ca,
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2'
}
})
await assert.rejects(
client.request({ path: '/', method: 'GET' }),
{ code: 'ERR_TLS_CERT_ALTNAME_INVALID' }
)
})
}Note the use of constants.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION in the server configuration. If TLS session resumption is permitted during renegotiation, OpenSSL may reuse the previously authenticated session state without re-evaluating the peer certificate chain. Setting SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION forces a full renegotiation handshake, ensuring the client parses and validates the newly swapped wrongHost certificate.
The primary impact of CVE-2026-87018 is a complete Denial of Service (DoS) of the target Node.js process, reflected in its CVSS v3.1 score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Because Node.js operates on a single-threaded event loop model, an uncaught AssertionError does not merely terminate the offending HTTP request; it halts the entire V8 isolate and exits the OS process.
In a multi-tenant web server or microservice architecture, a single process crash aborts all concurrent incoming HTTP requests, drops active WebSocket connections, and interrupts background job queues executing within the same runtime. Furthermore, if an application automatically retries failed webhook deliveries or queue jobs upon restart, a malicious endpoint can repeatedly trigger the crash every time the service comes back online, creating a persistent crash loop.
Standard application-level error handling—such as wrapping await fetch(url) or await undici.request(url) inside a try/catch block—does not prevent process termination. Because the assert() failure occurs inside an internal socket event listener attached directly to the tls.TLSSocket instance rather than within the promise execution chain of fetch(), the exception bypasses promise rejection handlers entirely.
Threat intelligence metrics indicate an EPSS probability score of 0.00187 (0.19%, 8.24th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. The low EPSS score aligns with the nature of client-side vulnerabilities: automated internet-wide scanners cannot directly probe a listening port to trigger the bug, as exploitation requires prompting the target application to initiate an outbound HTTPS connection.
The primary remediation is to upgrade undici to a patched release that removes the reachable assertions from the HTTP/1.1 and HTTP/2 socket error handlers. The OpenJS Foundation has released fixes across all maintained release branches:
8.12.2 or later.7.30.1 or later.6.29.1 or later.For applications that depend on undici directly via package.json, update the dependency using npm update undici and verify the resolved version in package-lock.json. For applications relying on the global fetch() API bundled within the Node.js runtime, ensure the Node.js runtime itself is updated to a distribution release that incorporates the patched undici versions, or explicitly override the global dispatcher using a patched userland undici package.
const { Agent, setGlobalDispatcher } = require('undici')
// Workaround: Enforce TLS 1.3 globally to prevent TLS 1.2 renegotiation
const tls13Agent = new Agent({
connect: {
minVersion: 'TLSv1.3'
}
})
setGlobalDispatcher(tls13Agent)If immediate patching is not feasible, administrators and developers can mitigate the vulnerability by enforcing a minimum TLS version of TLSv1.3 on outbound connections. Because TLS 1.3 prohibits session renegotiation at the protocol level, a remote server cannot perform the post-handshake certificate swap required to trigger ERR_TLS_CERT_ALTNAME_INVALID on an established connection. However, organizations implementing this workaround should verify that all required upstream HTTPS endpoints support TLS 1.3 to avoid breaking legitimate integrations.
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 | >= 4.2.1, < 6.29.1 | 6.29.1 |
undici Node.js / OpenJS Foundation | >= 7.0.0, < 7.30.1 | 7.30.1 |
undici Node.js / OpenJS Foundation | >= 8.0.0, < 8.12.2 | 8.12.2 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Reachable Assertion / Uncaught Exception (CWE-617 / CWE-248) |
| Attack Vector | Network (Outbound HTTPS Client to Malicious TLS 1.2 Server) |
| CVSS v3.1 Score | 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.19% (8.24th Percentile) |
| Security Impact | Process-wide Denial of Service via unhandled AssertionError |
| Affected Protocols | HTTP/1.1 and HTTP/2 over TLS 1.2 |
| Exploit Status | Proof-of-Concept Available (Regression Test) |
| CISA KEV Status | Not Listed |
The software contains an assert() statement that can be reached and triggered with attacker-controlled inputs, causing the application to fail and resulting in an uncaught exception (CWE-248).
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.
CVE-2026-116688 (GHSA-rh54-vffm-5fvp) is a high-severity Cross-Site Scripting (XSS) vulnerability in the CKEditor 5 engine package (`@ckeditor/ckeditor5-engine` and the umbrella `ckeditor5` package). The flaw originates from an upstream prototype pollution vulnerability in the `es-toolkit` dependency (versions prior to 1.52.0) used to parse and process inline HTML `style` attributes. When processing crafted CSS style declarations, the engine permits modification of `Object.prototype`, which alters downstream DOM conversion and sanitization checks to achieve arbitrary JavaScript execution in the browser context.
CVE-2026-116687 is a Cross-Site Scripting (XSS) vulnerability in the @ckeditor/ckeditor5-engine package of CKEditor 5. When the General HTML Support (GHS) plugin is configured to permit embedded container elements such as <object>, <embed>, or <iframe>, insufficient validation of data: URIs and incomplete stripping of ASCII C0 control characters allow attackers to execute arbitrary JavaScript within an isolated null-origin browser context.
CVE-2026-116686 is a Server-Side Request Forgery (SSRF) and Denial of Service (DoS) vulnerability in the official Model Context Protocol (MCP) Python SDK (mcp package on PyPI). When an MCP client validates structured tool execution results against a server-provided outputSchema, the SDK invokes the Python jsonschema library with its default URI resolution configuration. An untrusted or compromised MCP server can supply an outputSchema containing external $ref directives (using http://, https://, or file:// schemes), forcing the client process to synchronously fetch internal network resources, read local files, or block the main event loop indefinitely.