Skip to content

Commit fd890ba

Browse files
mcollinaaduh95
authored andcommitted
tls: bind reusable sessions to authenticated host
Backport-PR-URL: nodejs-private/node-private#895 PR-URL: nodejs-private/node-private#854 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> CVE-ID: CVE-2026-48934 Refs: https://hackerone.com/reports/3649802
1 parent c551a51 commit fd890ba

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
@@ -113,6 +113,82 @@ const kIsVerified = Symbol('verified');
113113

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

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

@@ -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

@@ -1131,9 +1208,19 @@ TLSSocket.prototype.setServername = function(name) {
11311208
};
11321209

11331210
TLSSocket.prototype.setSession = function(session) {
1134-
if (typeof session === 'string')
1135-
session = Buffer.from(session, 'latin1');
1136-
this._handle.setSession(session);
1211+
session = getSessionForReuse(session, this[kConnectOptions] || this._tlsOptions);
1212+
if (session !== undefined)
1213+
this._handle.setSession(session);
1214+
};
1215+
1216+
TLSSocket.prototype.getSession = function() {
1217+
if (!this._handle)
1218+
return null;
1219+
1220+
return wrapSessionState(
1221+
this._handle.getSession(),
1222+
this[kConnectOptions] || this._tlsOptions,
1223+
);
11371224
};
11381225

11391226
TLSSocket.prototype.getPeerCertificate = function(detailed) {
@@ -1192,7 +1279,6 @@ function makeSocketMethodProxy(name) {
11921279
'getFinished',
11931280
'getPeerFinished',
11941281
'getProtocol',
1195-
'getSession',
11961282
'getTLSTicket',
11971283
'isSessionReused',
11981284
'enableTrace',
@@ -1703,12 +1789,11 @@ function onConnectSecure() {
17031789
// Verify that server's identity matches it's certificate's names
17041790
// Unless server has resumed our existing session
17051791
if (!verifyError && !this.isSessionReused()) {
1706-
const hostname = options.servername ||
1707-
options.host ||
1708-
(options.socket?._host) ||
1709-
'localhost';
17101792
const cert = this.getPeerCertificate(true);
1711-
verifyError = options.checkServerIdentity(hostname, cert);
1793+
verifyError = options.checkServerIdentity(
1794+
getSessionServerIdentity(options),
1795+
cert,
1796+
);
17121797
}
17131798

17141799
if (verifyError) {
@@ -1785,6 +1870,8 @@ exports.connect = function connect(...args) {
17851870

17861871
const context = options.secureContext || tls.createSecureContext(options);
17871872

1873+
options.session = getSessionForReuse(options.session, options);
1874+
17881875
const tlssock = new TLSSocket(options.socket, {
17891876
allowHalfOpen: options.allowHalfOpen,
17901877
pipe: !!options.path,

0 commit comments

Comments
 (0)