Denial of Service (crash) via uncaught exception in TLS SNICallback during SNI processing
Description
The commit fixes a vulnerability where SNICallback invocation in loadSNI() could throw synchronously and remain uncaught, allowing a remote attacker to crash a TLS server by sending a crafted TLS ClientHello with a crafted server_name. The code previously invoked owner._SNICallback(...) without guarding against exceptions; the fix wraps the invocation in a try/catch and routes any error to owner.destroy(), completing CVE-2026-21637. This matches the triage and diff changes, and the test updates verify proper tlsClientError emission.
Proof of Concept
PoC to reproduce on pre-fix Node.js:\n\nServer (vulnerable):\nconst tls = require('tls');\nconst fs = require('fs');\nconst server = tls.createServer({\n key: fs.readFileSync('server-key.pem'),\n cert: fs.readFileSync('server-cert.pem'),\n SNICallback: (servername, cb) => {\n if (servername === 'evil.example.com') throw new Error('boom');\n cb(null, tls.createSecureContext({ key: fs.readFileSync('server-key.pem'), cert: fs.readFileSync('server-cert.pem') }));\n }\n});\nserver.listen(8443, () => console.log('listening'));\n\nClient:\nconst tls = require('tls');\nconst client = tls.connect({ port: 8443, host: 'localhost', servername: 'evil.example.com', rejectUnauthorized: false }, () => {\n client.end();\n});\nclient.on('error', (e) => console.error('client error', e));\n\nExpected: The server process will crash due to an uncaught exception thrown from SNICallback.\n\nNote: In the patched version, the exception is caught by the surrounding try/catch and the server terminates the connection gracefully via owner.destroy(), avoiding a crash.
Commit Details
Author: Matteo Collina
Date: 2026-02-17 13:26 UTC
Message:
tls: wrap SNICallback invocation in try/catch
Wrap the owner._SNICallback() invocation in loadSNI() with try/catch
to route exceptions through owner.destroy() instead of letting them
become uncaught exceptions. This completes the fix from CVE-2026-21637
which added try/catch protection to callALPNCallback,
onPskServerCallback, and onPskClientCallback but missed loadSNI().
Without this fix, a remote unauthenticated attacker can crash any
Node.js TLS server whose SNICallback may throw on unexpected input
by sending a single TLS ClientHello with a crafted server_name value.
Fixes: https://hackerone.com/reports/3556769
Refs: https://hackerone.com/reports/3473882
CVE-ID: CVE-2026-21637
PR-URL: https://github.com/nodejs-private/node-private/pull/819
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
CVE-ID: CVE-2026-21637
Triage Assessment
Vulnerability Type: Denial of Service (crash) / crash due to uncaught exception in TLS SNICallback
Confidence: HIGH
Reasoning:
Commit wraps SNICallback invocation in loadSNI() with a try/catch and routes exceptions to owner.destroy(), addressing CVE-2026-21637. Previously, an exception thrown by SNICallback could be uncaught, allowing a remote attacker to crash a TLS server by sending a crafted ClientHello/server_name. The changes complete the fix by ensuring errors in SNICallback are handled safely.
Verification Assessment
Vulnerability Type: Denial of Service (crash) via uncaught exception in TLS SNICallback during SNI processing
Confidence: HIGH
Affected Versions: Node.js 25.x prior to 25.9.0
Code Diff
diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js
index 308180d483908d..d89e501432968a 100644
--- a/lib/internal/tls/wrap.js
+++ b/lib/internal/tls/wrap.js
@@ -210,23 +210,27 @@ function loadSNI(info) {
return requestOCSP(owner, info);
let once = false;
- owner._SNICallback(servername, (err, context) => {
- if (once)
- return owner.destroy(new ERR_MULTIPLE_CALLBACK());
- once = true;
+ try {
+ owner._SNICallback(servername, (err, context) => {
+ if (once)
+ return owner.destroy(new ERR_MULTIPLE_CALLBACK());
+ once = true;
- if (err)
- return owner.destroy(err);
+ if (err)
+ return owner.destroy(err);
- if (owner._handle === null)
- return owner.destroy(new ERR_SOCKET_CLOSED());
+ if (owner._handle === null)
+ return owner.destroy(new ERR_SOCKET_CLOSED());
- // TODO(indutny): eventually disallow raw `SecureContext`
- if (context)
- owner._handle.sni_context = context.context || context;
+ // TODO(indutny): eventually disallow raw `SecureContext`
+ if (context)
+ owner._handle.sni_context = context.context || context;
- requestOCSP(owner, info);
- });
+ requestOCSP(owner, info);
+ });
+ } catch (err) {
+ owner.destroy(err);
+ }
}
diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js
index e87b68d778035c..881215672ecd0d 100644
--- a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js
+++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js
@@ -332,4 +332,94 @@ describe('TLS callback exception handling', () => {
await promise;
});
+
+ // Test 7: SNI callback throwing should emit tlsClientError
+ it('SNICallback throwing emits tlsClientError', async (t) => {
+ const server = tls.createServer({
+ key: fixtures.readKey('agent2-key.pem'),
+ cert: fixtures.readKey('agent2-cert.pem'),
+ SNICallback: (servername, cb) => {
+ throw new Error('Intentional SNI callback error');
+ },
+ });
+
+ t.after(() => server.close());
+
+ const { promise, resolve, reject } = createTestPromise();
+
+ server.on('tlsClientError', common.mustCall((err, socket) => {
+ try {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Intentional SNI callback error');
+ socket.destroy();
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }));
+
+ server.on('secureConnection', () => {
+ reject(new Error('secureConnection should not fire'));
+ });
+
+ await new Promise((res) => server.listen(0, res));
+
+ const client = tls.connect({
+ port: server.address().port,
+ host: '127.0.0.1',
+ servername: 'evil.attacker.com',
+ rejectUnauthorized: false,
+ });
+
+ client.on('error', () => {});
+
+ await promise;
+ });
+
+ // Test 8: SNI callback with validation error should emit tlsClientError
+ it('SNICallback validation error emits tlsClientError', async (t) => {
+ const server = tls.createServer({
+ key: fixtures.readKey('agent2-key.pem'),
+ cert: fixtures.readKey('agent2-cert.pem'),
+ SNICallback: (servername, cb) => {
+ // Simulate common developer pattern: throw on unknown servername
+ if (servername !== 'expected.example.com') {
+ throw new Error(`Unknown servername: ${servername}`);
+ }
+ cb(null, null);
+ },
+ });
+
+ t.after(() => server.close());
+
+ const { promise, resolve, reject } = createTestPromise();
+
+ server.on('tlsClientError', common.mustCall((err, socket) => {
+ try {
+ assert.ok(err instanceof Error);
+ assert.ok(err.message.includes('Unknown servername'));
+ socket.destroy();
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }));
+
+ server.on('secureConnection', () => {
+ reject(new Error('secureConnection should not fire'));
+ });
+
+ await new Promise((res) => server.listen(0, res));
+
+ const client = tls.connect({
+ port: server.address().port,
+ host: '127.0.0.1',
+ servername: 'unexpected.domain.com',
+ rejectUnauthorized: false,
+ });
+
+ client.on('error', () => {});
+
+ await promise;
+ });
});