Node.js v22.23.5, v24.22.3 and v26.13.1 fix ways a per-hostname client-certificate policy could be crossed. What changed, how to triage and design changes worth making.
What shipped and who is affected
On Wednesday 11 November 2026 the Node.js project published security releases for all three active lines: v22.23.5, v24.22.3 and v26.13.1. The advisory lists sixteen CVEs, the highest rated high. Many are HTTP/2 memory-safety and denial-of-service fixes that every server on an affected line should take. This guide concentrates on a cluster that changes how you should think about one specific design: a single TLS server that applies different client-certificate rules to different hostnames.
That design is common. A service at Belgavi's Book Store might serve api.books.example.test to partner systems with client certificates from a partner CA, and admin.books.example.test on the same listener to staff whose certificates come from a separate staff CA. The server picks the right policy from the server name the client sends in the TLS handshake (SNI). The November advisory describes several ways that per-name policy could be crossed:
- CVE-2026-123872, rated high, affects 22.x, 24.x and 26.x. The advisory describes a client authenticated for a permitted host renegotiating a TLS 1.2 connection under a different server name and carrying its existing authentication to a host with a stricter client-certificate policy. The release notes list three changes under this CVE: renegotiation and HelloRetryRequest may no longer change SNI, TLS resumption may no longer change SNI, and resumption is disabled for mTLS servers that have no session id context.
- CVE-2026-123867, rated medium, affects 22.x, 24.x and 26.x. When a hostname is given its own client CA with
server.addContext(host, { ca: protectedCA })but no server certificate in that context, the advisory says the per-host CA was never installed and the handshake fell back to the default client CA. A client certified only by the default CA could complete mTLS to the protected name, while a client certified by the protected CA was rejected. - CVE-2026-123877, rated medium, affects 22.x, 24.x and 26.x. This one is on the client side.
https.Agentgave the same agent and session-cache key to a request that omitscaand to one that passesca: [], although the first uses the default trust store and the second replaces it. A session authenticated under the broader policy could then be resumed by the stricter request, which reportedauthorized === true. - CVE-2026-126051, rated high, affects 24.x and 26.x. A remote client could crash TLS servers that use
SNICallback, OCSP or session hooks, orserver.addContext(), by destroying the socket while a deferred handshake callback was pending. This is the one in the cluster that does not cross a policy; it terminates the process.
The CVE numbers, ratings and affected lines above are copied from the advisory, which assigns them per issue; check it for the lines you run rather than assuming every fix applies to every line.
Why per-host policy is fragile
A TLS server decides who the client is during the handshake and then, in most frameworks, never asks again. Per-host policy adds a second question: which name was this identity checked against? Each of the issues above is a path where the answer changed after the check.
Renegotiation in TLS 1.2 runs a new handshake inside an existing connection. If the second handshake may name a different server, the identity checked in the first handshake can be presented to the second name's application logic. TLS 1.3 removed renegotiation, but resumption remains: a client presents a ticket or session id from an earlier connection and skips the certificate exchange. If resumption is allowed across names, a session established under the permissive name's policy is accepted under the strict name. Session id context exists for exactly this reason: it labels sessions so they are only resumed in the same context, and Node.js's tls.createServer documents sessionIdContext as an opaque identifier servers use to ensure session state is not shared between applications.
The CA-only context bug is different in kind: there was never a correct check for the protected name. The configuration looked right and failed open, which is the hardest class of bug to notice, because nothing errors and the permitted clients still work.
Confirming the client-side key collision locally
The https.Agent issue is easy to observe without a network. On an unpatched binary, Node.js v22.17.0 in the companion run, the agent produces identical keys for the two policies:
const https = require('node:https');
const agent = new https.Agent();
const base = { host: 'orders.example.test', port: 443 };
agent.getName({ ...base }); // 'orders.example.test:443:::::::::::::::::::::'
agent.getName({ ...base, ca: [] }); // 'orders.example.test:443:::::::::::::::::::::'
agent.getName({ ...base, ca: ['X'] }); // 'orders.example.test:443::X:::::::::::::::::::'The release notes describe the fix as isolating Agent TLS policy keys. This guide did not run a patched binary, so it does not show the fixed output; run the same three lines after upgrading and confirm the first two now differ. The companion's test is written to require the collision only when the running version is below the fixed release for its line.
Triage in order
- Upgrade. Every server and client process on 22.x, 24.x or 26.x should move to v22.23.5, v24.22.3 or v26.13.1 or later. End-of-life lines are always affected when a security release occurs, as the advisory notes; they get no fix.
- Find per-host TLS policy. Search for
addContext(,SNICallbackandrequestCert: truein server code and in any framework configuration that wraps them. Any listener that serves more than one name with differentcavalues or differentrequestCertsettings is in scope for CVE-2026-123872 and possibly CVE-2026-123867. - Find CA-only contexts. An
addContextcall whose options includecabut nocertandkeyis the exact pattern in CVE-2026-123867. On an unpatched server, assume the protected name accepted any client the default CA trusts. - Find clients that vary
caper request. Code that passesca: []or other narrowed trust stores through a shared agent, such as a pinned-CA call to a payment provider alongside ordinary calls, is the CVE-2026-123877 pattern. - Review logs. For names with a stricter policy, look for successful requests whose client-certificate subject or issuer is not one the strict policy should accept. That is the only after-the-fact evidence of a crossed policy; whether you have it depends on whether you logged certificate details per request.
The companion's audit script turns steps 2 to 4 into checks over a declarative description of a server's TLS options. It reports the running version against the fixed releases, the agent key collision, and findings for CA-only contexts, mixed per-host client CAs on one listener, and mTLS without a session id context.
Design changes worth making anyway
Patching closes the specific paths. The structural lesson is that a security boundary between two populations of clients should not depend on per-connection state that the client can influence after authentication. Three changes make the boundary sturdier.
Separate listeners for separate client populations. If staff and partners need different client CAs, give them different ports or different IP addresses, each with one policy. Renegotiation, resumption and SNI then cannot cross the boundary, because there is nothing on the other side of it in the same process state.
Authorise per request, not per connection. After the handshake, the application should check the client certificate against the hostname of each request, using the certificate the TLS layer actually verified. If the Host header names admin.books.example.test, the code that serves it should confirm the peer certificate chains to the staff CA, and refuse otherwise. This also catches a mismatch between SNI and the Host header, which HTTP clients can produce.
Set the session context explicitly. The Node.js TLS documentation notes that tls.createServer() derives a default sessionIdContext from process.argv, while other APIs that create secure contexts have no default; a default shared by every name on the listener does not separate their policies. If one process must serve several policies, give each policy its own SecureContext with its own sessionIdContext, and prefer TLS 1.3 only, which removes renegotiation entirely. Measure whether you need session resumption at all for client-certificate traffic; for low-volume staff endpoints the handshake cost is negligible.
Verifying after the upgrade
- Confirm
process.versionon every running instance, not only in the image definition. - Run the three
getNamelines on a patched binary and check that omittingcaand passingca: []now give different keys. - In a test environment, configure a listener with a CA-only context for a protected name and confirm a client certified only by the default CA is now rejected for that name.
- For a mixed-policy listener, attempt a resumed connection under a different server name from a test client and confirm it falls back to a full handshake with the protected name's policy.
- Keep the HTTP/2 fixes in view. CVE-2026-123863, CVE-2026-123864, CVE-2026-123865 and CVE-2026-123866 are rated high, and they matter for every server that exposes HTTP/2 or HTTPS on an affected line, whether or not it uses client certificates.
Run the example yourself
An offline audit: version status per release line, the https.Agent key check and findings for the per-host mTLS patterns, with node --test tests. Companion code on GitHub; its README lists the prerequisites, exact commands, expected output and limits.