Skip to content

Commit d1be630

Browse files
mcollinaaduh95
authored andcommitted
tls: bind reusable sessions to authenticated host
PR-URL: nodejs-private/node-private#854 Reviewed-By: Robert Nagy <ronagy@icloud.com> CVE-ID: CVE-2026-48934 Refs: https://hackerone.com/reports/3649802
1 parent c8668be commit d1be630

1 file changed

Lines changed: 98 additions & 11 deletions

File tree

lib/internal/tls/wrap.js

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,82 @@ const kIsVerified = Symbol('verified');
114114

115115
const noop = () => {};
116116

117+
const kTLSSessionStatePrefix = Buffer.from('\0nodejs:tls:session:1\0');
118+
119+
function getSessionServerIdentity(options) {
120+
return options?.servername ||
121+
options?.host ||
122+
options?.socket?._host ||
123+
'localhost';
124+
}
125+
126+
function wrapSessionState(session, options) {
127+
if (!Buffer.isBuffer(session) || options?.isServer)
128+
return session;
129+
130+
const servername = Buffer.from(getSessionServerIdentity(options), 'utf8');
131+
const servernameLength = Buffer.allocUnsafe(2);
132+
servernameLength.writeUInt16BE(servername.length, 0);
133+
134+
return Buffer.concat([
135+
kTLSSessionStatePrefix,
136+
servernameLength,
137+
servername,
138+
session,
139+
]);
140+
}
141+
142+
function unwrapSessionState(session) {
143+
if (!Buffer.isBuffer(session) ||
144+
session.length < kTLSSessionStatePrefix.length + 2 ||
145+
Buffer.compare(
146+
session.subarray(0, kTLSSessionStatePrefix.length),
147+
kTLSSessionStatePrefix,
148+
) !== 0) {
149+
return;
150+
}
151+
152+
const start = kTLSSessionStatePrefix.length;
153+
const servernameLength = session.readUInt16BE(start);
154+
const servernameStart = start + 2;
155+
const servernameEnd = servernameStart + servernameLength;
156+
if (session.length < servernameEnd)
157+
return;
158+
159+
return {
160+
servername: session.toString('utf8', servernameStart, servernameEnd),
161+
session: session.subarray(servernameEnd),
162+
};
163+
}
164+
165+
function getSessionForReuse(session, options) {
166+
if (typeof session === 'string')
167+
session = Buffer.from(session, 'latin1');
168+
169+
if (options?.isServer)
170+
return session;
171+
172+
const wrappedSession = unwrapSessionState(session);
173+
if (wrappedSession !== undefined) {
174+
const servername = getSessionServerIdentity(options);
175+
if (wrappedSession.servername !== servername) {
176+
debug('ignore session for %s: authenticated for %s',
177+
servername, wrappedSession.servername);
178+
return;
179+
}
180+
181+
return wrappedSession.session;
182+
}
183+
184+
if (Buffer.isBuffer(session) && options?.rejectUnauthorized !== false) {
185+
debug('ignore raw session for verified client connection to %s',
186+
getSessionServerIdentity(options));
187+
return;
188+
}
189+
190+
return session;
191+
}
192+
117193
let tlsTracingWarned = false;
118194

119195
// Server side times how long a handshake is taking to protect against slow
@@ -341,10 +417,11 @@ function requestOCSPDone(socket) {
341417
function onnewsessionclient(sessionId, session) {
342418
debug('client emit session');
343419
const owner = this[owner_symbol];
420+
const wrappedSession = wrapSessionState(session, owner[kConnectOptions]);
344421
if (owner[kIsVerified]) {
345-
owner.emit('session', session);
422+
owner.emit('session', wrappedSession);
346423
} else {
347-
owner[kPendingSession] = session;
424+
owner[kPendingSession] = wrappedSession;
348425
}
349426
}
350427

@@ -1139,9 +1216,19 @@ TLSSocket.prototype.setServername = function(name) {
11391216
};
11401217

11411218
TLSSocket.prototype.setSession = function(session) {
1142-
if (typeof session === 'string')
1143-
session = Buffer.from(session, 'latin1');
1144-
this._handle.setSession(session);
1219+
session = getSessionForReuse(session, this[kConnectOptions] || this._tlsOptions);
1220+
if (session !== undefined)
1221+
this._handle.setSession(session);
1222+
};
1223+
1224+
TLSSocket.prototype.getSession = function() {
1225+
if (!this._handle)
1226+
return null;
1227+
1228+
return wrapSessionState(
1229+
this._handle.getSession(),
1230+
this[kConnectOptions] || this._tlsOptions,
1231+
);
11451232
};
11461233

11471234
TLSSocket.prototype.getPeerCertificate = function(detailed) {
@@ -1200,7 +1287,6 @@ function makeSocketMethodProxy(name) {
12001287
'getFinished',
12011288
'getPeerFinished',
12021289
'getProtocol',
1203-
'getSession',
12041290
'getTLSTicket',
12051291
'isSessionReused',
12061292
'enableTrace',
@@ -1663,12 +1749,11 @@ function onConnectSecure() {
16631749
// Verify that server's identity matches it's certificate's names
16641750
// Unless server has resumed our existing session
16651751
if (!verifyError && !this.isSessionReused()) {
1666-
const hostname = options.servername ||
1667-
options.host ||
1668-
(options.socket?._host) ||
1669-
'localhost';
16701752
const cert = this.getPeerCertificate(true);
1671-
verifyError = options.checkServerIdentity(hostname, cert);
1753+
verifyError = options.checkServerIdentity(
1754+
getSessionServerIdentity(options),
1755+
cert,
1756+
);
16721757
}
16731758

16741759
if (verifyError) {
@@ -1753,6 +1838,8 @@ exports.connect = function connect(...args) {
17531838
);
17541839
}
17551840

1841+
options.session = getSessionForReuse(options.session, options);
1842+
17561843
const tlssock = new TLSSocket(options.socket, {
17571844
allowHalfOpen: options.allowHalfOpen,
17581845
pipe: !!options.path,

0 commit comments

Comments
 (0)