Skip to content

Commit 25d6799

Browse files
mcollinamarco-ippolito
authored andcommitted
tls: route callback exceptions through error handlers
Wrap pskCallback and ALPNCallback invocations in try-catch blocks to route exceptions through owner.destroy() instead of letting them become uncaught exceptions. This prevents remote attackers from crashing TLS servers or causing resource exhaustion. Fixes: https://hackerone.com/reports/3473882 PR-URL: nodejs-private/node-private#782 PR-URL: nodejs-private/node-private#796 Reviewed-By: Matteo Collina <[email protected]> CVE-ID: CVE-2026-21637
1 parent 37509c3 commit 25d6799

File tree

3 files changed

+442
-81
lines changed

3 files changed

+442
-81
lines changed

lib/_tls_wrap.js

Lines changed: 87 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -235,39 +235,44 @@ function callALPNCallback(protocolsBuffer) {
235235
const handle = this;
236236
const socket = handle[owner_symbol];
237237

238-
const servername = handle.getServername();
238+
try {
239+
const servername = handle.getServername();
239240

240-
// Collect all the protocols from the given buffer:
241-
const protocols = [];
242-
let offset = 0;
243-
while (offset < protocolsBuffer.length) {
244-
const protocolLen = protocolsBuffer[offset];
245-
offset += 1;
241+
// Collect all the protocols from the given buffer:
242+
const protocols = [];
243+
let offset = 0;
244+
while (offset < protocolsBuffer.length) {
245+
const protocolLen = protocolsBuffer[offset];
246+
offset += 1;
246247

247-
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
248-
offset += protocolLen;
248+
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
249+
offset += protocolLen;
249250

250-
protocols.push(protocol.toString('ascii'));
251-
}
251+
protocols.push(protocol.toString('ascii'));
252+
}
252253

253-
const selectedProtocol = socket[kALPNCallback]({
254-
servername,
255-
protocols,
256-
});
254+
const selectedProtocol = socket[kALPNCallback]({
255+
servername,
256+
protocols,
257+
});
257258

258-
// Undefined -> all proposed protocols rejected
259-
if (selectedProtocol === undefined) return undefined;
259+
// Undefined -> all proposed protocols rejected
260+
if (selectedProtocol === undefined) return undefined;
260261

261-
const protocolIndex = protocols.indexOf(selectedProtocol);
262-
if (protocolIndex === -1) {
263-
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
264-
}
265-
let protocolOffset = 0;
266-
for (let i = 0; i < protocolIndex; i++) {
267-
protocolOffset += 1 + protocols[i].length;
268-
}
262+
const protocolIndex = protocols.indexOf(selectedProtocol);
263+
if (protocolIndex === -1) {
264+
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
265+
}
266+
let protocolOffset = 0;
267+
for (let i = 0; i < protocolIndex; i++) {
268+
protocolOffset += 1 + protocols[i].length;
269+
}
269270

270-
return protocolOffset;
271+
return protocolOffset;
272+
} catch (err) {
273+
socket.destroy(err);
274+
return undefined;
275+
}
271276
}
272277

273278
function requestOCSP(socket, info) {
@@ -374,63 +379,75 @@ function onnewsession(sessionId, session) {
374379

375380
function onPskServerCallback(identity, maxPskLen) {
376381
const owner = this[owner_symbol];
377-
const ret = owner[kPskCallback](owner, identity);
378-
if (ret == null)
379-
return undefined;
380382

381-
let psk;
382-
if (isArrayBufferView(ret)) {
383-
psk = ret;
384-
} else {
385-
if (typeof ret !== 'object') {
386-
throw new ERR_INVALID_ARG_TYPE(
387-
'ret',
388-
['Object', 'Buffer', 'TypedArray', 'DataView'],
389-
ret,
383+
try {
384+
const ret = owner[kPskCallback](owner, identity);
385+
if (ret == null)
386+
return undefined;
387+
388+
let psk;
389+
if (isArrayBufferView(ret)) {
390+
psk = ret;
391+
} else {
392+
if (typeof ret !== 'object') {
393+
throw new ERR_INVALID_ARG_TYPE(
394+
'ret',
395+
['Object', 'Buffer', 'TypedArray', 'DataView'],
396+
ret,
397+
);
398+
}
399+
psk = ret.psk;
400+
validateBuffer(psk, 'psk');
401+
}
402+
403+
if (psk.length > maxPskLen) {
404+
throw new ERR_INVALID_ARG_VALUE(
405+
'psk',
406+
psk,
407+
`Pre-shared key exceeds ${maxPskLen} bytes`,
390408
);
391409
}
392-
psk = ret.psk;
393-
validateBuffer(psk, 'psk');
394-
}
395410

396-
if (psk.length > maxPskLen) {
397-
throw new ERR_INVALID_ARG_VALUE(
398-
'psk',
399-
psk,
400-
`Pre-shared key exceeds ${maxPskLen} bytes`,
401-
);
411+
return psk;
412+
} catch (err) {
413+
owner.destroy(err);
414+
return undefined;
402415
}
403-
404-
return psk;
405416
}
406417

407418
function onPskClientCallback(hint, maxPskLen, maxIdentityLen) {
408419
const owner = this[owner_symbol];
409-
const ret = owner[kPskCallback](hint);
410-
if (ret == null)
411-
return undefined;
412420

413-
validateObject(ret, 'ret');
421+
try {
422+
const ret = owner[kPskCallback](hint);
423+
if (ret == null)
424+
return undefined;
425+
426+
validateObject(ret, 'ret');
427+
428+
validateBuffer(ret.psk, 'psk');
429+
if (ret.psk.length > maxPskLen) {
430+
throw new ERR_INVALID_ARG_VALUE(
431+
'psk',
432+
ret.psk,
433+
`Pre-shared key exceeds ${maxPskLen} bytes`,
434+
);
435+
}
414436

415-
validateBuffer(ret.psk, 'psk');
416-
if (ret.psk.length > maxPskLen) {
417-
throw new ERR_INVALID_ARG_VALUE(
418-
'psk',
419-
ret.psk,
420-
`Pre-shared key exceeds ${maxPskLen} bytes`,
421-
);
422-
}
437+
validateString(ret.identity, 'identity');
438+
if (Buffer.byteLength(ret.identity) > maxIdentityLen) {
439+
throw new ERR_INVALID_ARG_VALUE(
440+
'identity',
441+
ret.identity,
442+
`PSK identity exceeds ${maxIdentityLen} bytes`,
443+
);
444+
}
423445

424-
validateString(ret.identity, 'identity');
425-
if (Buffer.byteLength(ret.identity) > maxIdentityLen) {
426-
throw new ERR_INVALID_ARG_VALUE(
427-
'identity',
428-
ret.identity,
429-
`PSK identity exceeds ${maxIdentityLen} bytes`,
430-
);
446+
return { psk: ret.psk, identity: ret.identity };
447+
} catch (err) {
448+
owner.destroy(err);
449+
return undefined;
431450
}
432-
433-
return { psk: ret.psk, identity: ret.identity };
434451
}
435452

436453
function onkeylog(line) {

test/parallel/test-tls-alpn-server-client.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -253,25 +253,35 @@ function TestALPNCallback() {
253253
function TestBadALPNCallback() {
254254
// Server always returns a fixed invalid value:
255255
const serverOptions = {
256+
key: loadPEM('agent2-key'),
257+
cert: loadPEM('agent2-cert'),
256258
ALPNCallback: common.mustCall(() => 'http/5')
257259
};
258260

259-
const clientsOptions = [{
260-
ALPNProtocols: ['http/1', 'h2'],
261-
}];
261+
const server = tls.createServer(serverOptions);
262262

263-
process.once('uncaughtException', common.mustCall((error) => {
263+
// Error should be emitted via tlsClientError, not as uncaughtException
264+
server.on('tlsClientError', common.mustCall((error, socket) => {
264265
assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
266+
socket.destroy();
265267
}));
266268

267-
runTest(clientsOptions, serverOptions, function(results) {
268-
// Callback returns 'http/5' => doesn't match client ALPN => error & reset
269-
assert.strictEqual(results[0].server, undefined);
270-
const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL'];
271-
assert.ok(allowedErrors.includes(results[0].client.error.code), `'${results[0].client.error.code}' was not one of ${allowedErrors}.`);
269+
server.listen(0, serverIP, common.mustCall(() => {
270+
const client = tls.connect({
271+
port: server.address().port,
272+
host: serverIP,
273+
rejectUnauthorized: false,
274+
ALPNProtocols: ['http/1', 'h2'],
275+
}, common.mustNotCall());
272276

273-
TestALPNOptionsCallback();
274-
});
277+
client.on('error', common.mustCall((err) => {
278+
// Client gets reset when server handles error via tlsClientError
279+
const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL'];
280+
assert.ok(allowedErrors.includes(err.code), `'${err.code}' was not one of ${allowedErrors}.`);
281+
server.close();
282+
TestALPNOptionsCallback();
283+
}));
284+
}));
275285
}
276286

277287
function TestALPNOptionsCallback() {

0 commit comments

Comments
 (0)