]> dgit.raspbian.org Git - nodejs.git/commitdiff
tls: bind reusable sessions to authenticated host
authorMatteo Collina <hello@matteocollina.com>
Mon, 13 Apr 2026 07:53:48 +0000 (09:53 +0200)
committerBastien Roucariès <rouca@debian.org>
Sun, 16 Aug 2026 17:34:06 +0000 (19:34 +0200)
Backport-PR-URL: https://github.com/nodejs-private/node-private/pull/895
PR-URL: https://github.com/nodejs-private/node-private/pull/854
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
CVE-ID: CVE-2026-48934
Refs: https://hackerone.com/reports/3649802
origin: backport, https://github.com/nodejs/node/commit/fd890ba01d508ac111bbba302981d7fdf734d2ce
bug: https://nodejs.org/en/blog/vulnerability/june-2026-security-releases#tls-host-identity-verification-bypass-via-session-reuse-with-different-servername-leads-to-unauthorized-connections-cve-2026-48934---medium

Gbp-Pq: Topic sec
Gbp-Pq: Name CVE-2026-48934.patch

lib/_tls_wrap.js

index 410de6c9e2d7fbe1c9d09e1f9cc664fd52bae920..95f74f82768fe5cd085f712ae9aac541223a4fd7 100644 (file)
@@ -119,6 +119,82 @@ const kIsVerified = Symbol('verified');
 
 const noop = FunctionPrototype;
 
+const kTLSSessionStatePrefix = Buffer.from('\0nodejs:tls:session:1\0');
+
+function getSessionServerIdentity(options) {
+  return options?.servername ||
+         options?.host ||
+         options?.socket?._host ||
+         'localhost';
+}
+
+function wrapSessionState(session, options) {
+  if (!Buffer.isBuffer(session) || options?.isServer)
+    return session;
+
+  const servername = Buffer.from(getSessionServerIdentity(options), 'utf8');
+  const servernameLength = Buffer.allocUnsafe(2);
+  servernameLength.writeUInt16BE(servername.length, 0);
+
+  return Buffer.concat([
+    kTLSSessionStatePrefix,
+    servernameLength,
+    servername,
+    session,
+  ]);
+}
+
+function unwrapSessionState(session) {
+  if (!Buffer.isBuffer(session) ||
+      session.length < kTLSSessionStatePrefix.length + 2 ||
+      Buffer.compare(
+        session.subarray(0, kTLSSessionStatePrefix.length),
+        kTLSSessionStatePrefix,
+      ) !== 0) {
+    return;
+  }
+
+  const start = kTLSSessionStatePrefix.length;
+  const servernameLength = session.readUInt16BE(start);
+  const servernameStart = start + 2;
+  const servernameEnd = servernameStart + servernameLength;
+  if (session.length < servernameEnd)
+    return;
+
+  return {
+    servername: session.toString('utf8', servernameStart, servernameEnd),
+    session: session.subarray(servernameEnd),
+  };
+}
+
+function getSessionForReuse(session, options) {
+  if (typeof session === 'string')
+    session = Buffer.from(session, 'latin1');
+
+  if (options?.isServer)
+    return session;
+
+  const wrappedSession = unwrapSessionState(session);
+  if (wrappedSession !== undefined) {
+    const servername = getSessionServerIdentity(options);
+    if (wrappedSession.servername !== servername) {
+      debug('ignore session for %s: authenticated for %s',
+            servername, wrappedSession.servername);
+      return;
+    }
+
+    return wrappedSession.session;
+  }
+
+  if (Buffer.isBuffer(session) && options?.rejectUnauthorized !== false) {
+    debug('ignore raw session for verified client connection to %s',
+          getSessionServerIdentity(options));
+    return;
+  }
+
+  return session;
+}
+
 let ipServernameWarned = false;
 let tlsTracingWarned = false;
 
@@ -347,10 +423,11 @@ function requestOCSPDone(socket) {
 function onnewsessionclient(sessionId, session) {
   debug('client emit session');
   const owner = this[owner_symbol];
+  const wrappedSession = wrapSessionState(session, owner[kConnectOptions]);
   if (owner[kIsVerified]) {
-    owner.emit('session', session);
+    owner.emit('session', wrappedSession);
   } else {
-    owner[kPendingSession] = session;
+    owner[kPendingSession] = wrappedSession;
   }
 }
 
@@ -1123,9 +1200,19 @@ TLSSocket.prototype.setServername = function(name) {
 };
 
 TLSSocket.prototype.setSession = function(session) {
-  if (typeof session === 'string')
-    session = Buffer.from(session, 'latin1');
-  this._handle.setSession(session);
+  session = getSessionForReuse(session, this[kConnectOptions] || this._tlsOptions);
+  if (session !== undefined)
+    this._handle.setSession(session);
+};
+
+TLSSocket.prototype.getSession = function() {
+  if (!this._handle)
+    return null;
+
+  return wrapSessionState(
+    this._handle.getSession(),
+    this[kConnectOptions] || this._tlsOptions,
+  );
 };
 
 TLSSocket.prototype.getPeerCertificate = function(detailed) {
@@ -1173,7 +1260,6 @@ ArrayPrototypeForEach([
   'getFinished',
   'getPeerFinished',
   'getProtocol',
-  'getSession',
   'getTLSTicket',
   'isSessionReused',
   'enableTrace',
@@ -1684,12 +1770,11 @@ function onConnectSecure() {
   // Verify that server's identity matches it's certificate's names
   // Unless server has resumed our existing session
   if (!verifyError && !this.isSessionReused()) {
-    const hostname = options.servername ||
-                   options.host ||
-                   (options.socket && options.socket._host) ||
-                   'localhost';
     const cert = this.getPeerCertificate(true);
-    verifyError = options.checkServerIdentity(hostname, cert);
+    verifyError = options.checkServerIdentity(
+      getSessionServerIdentity(options),
+      cert,
+    );
   }
 
   if (verifyError) {
@@ -1772,6 +1857,8 @@ exports.connect = function connect(...args) {
 
   const context = options.secureContext || tls.createSecureContext(options);
 
+  options.session = getSessionForReuse(options.session, options);
+
   const tlssock = new TLSSocket(options.socket, {
     allowHalfOpen: options.allowHalfOpen,
     pipe: !!options.path,