From: Anil Dongare <[email protected]>

Upstream Repository: https://github.com/nodejs/node.git

Bug Details: https://nvd.nist.gov/vuln/detail/CVE-2026-21637
Type: Security Fix
CVE: CVE-2026-21637
Score: 7.5
Patch: https://github.com/nodejs/node/commit/25d6799df65c

Signed-off-by: Anil Dongare <[email protected]>
---
 .../nodejs/nodejs/CVE-2026-21637.patch        | 616 ++++++++++++++++++
 .../recipes-devtools/nodejs/nodejs_20.18.2.bb |   1 +
 2 files changed, 617 insertions(+)
 create mode 100644 meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-21637.patch

diff --git a/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-21637.patch 
b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-21637.patch
new file mode 100644
index 0000000000..cdf23af7c1
--- /dev/null
+++ b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-21637.patch
@@ -0,0 +1,616 @@
+From ba66644cc25154598c4c3988abf812022e32b8e4 Mon Sep 17 00:00:00 2001
+From: Matteo Collina <[email protected]>
+Date: Mon, 22 Dec 2025 18:25:33 +0100
+Subject: [PATCH 4/6] 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: https://github.com/nodejs-private/node-private/pull/782
+PR-URL: https://github.com/nodejs-private/node-private/pull/796
+Reviewed-By: Matteo Collina <[email protected]>
+CVE-ID: CVE-2026-21637
+
+CVE: CVE-2026-21637
+Upstream-Status: Backport [https://github.com/nodejs/node/commit/25d6799df65c]
+
+(cherry picked from commit 25d6799df65cd08f5fd057cae23e00a81bf33757)
+Signed-off-by: Anil Dongare <[email protected]>
+---
+ lib/_tls_wrap.js                              | 157 ++++----
+ test/parallel/test-tls-alpn-server-client.js  |  32 +-
+ ...ls-psk-alpn-callback-exception-handling.js | 334 ++++++++++++++++++
+ 3 files changed, 442 insertions(+), 81 deletions(-)
+ create mode 100644 
test/parallel/test-tls-psk-alpn-callback-exception-handling.js
+
+diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
+index 4eb7b7ffa69..c3e48a6cbc8 100644
+--- a/lib/_tls_wrap.js
++++ b/lib/_tls_wrap.js
+@@ -233,39 +233,44 @@ function callALPNCallback(protocolsBuffer) {
+   const handle = this;
+   const socket = handle[owner_symbol];
+
+-  const servername = handle.getServername();
++  try {
++    const servername = handle.getServername();
+
+-  // Collect all the protocols from the given buffer:
+-  const protocols = [];
+-  let offset = 0;
+-  while (offset < protocolsBuffer.length) {
+-    const protocolLen = protocolsBuffer[offset];
+-    offset += 1;
++    // Collect all the protocols from the given buffer:
++    const protocols = [];
++    let offset = 0;
++    while (offset < protocolsBuffer.length) {
++      const protocolLen = protocolsBuffer[offset];
++      offset += 1;
+
+-    const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
+-    offset += protocolLen;
++      const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
++      offset += protocolLen;
+
+-    protocols.push(protocol.toString('ascii'));
+-  }
++      protocols.push(protocol.toString('ascii'));
++    }
+
+-  const selectedProtocol = socket[kALPNCallback]({
+-    servername,
+-    protocols,
+-  });
++    const selectedProtocol = socket[kALPNCallback]({
++      servername,
++      protocols,
++    });
+
+-  // Undefined -> all proposed protocols rejected
+-  if (selectedProtocol === undefined) return undefined;
++    // Undefined -> all proposed protocols rejected
++    if (selectedProtocol === undefined) return undefined;
+
+-  const protocolIndex = protocols.indexOf(selectedProtocol);
+-  if (protocolIndex === -1) {
+-    throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, 
protocols);
+-  }
+-  let protocolOffset = 0;
+-  for (let i = 0; i < protocolIndex; i++) {
+-    protocolOffset += 1 + protocols[i].length;
+-  }
++    const protocolIndex = protocols.indexOf(selectedProtocol);
++    if (protocolIndex === -1) {
++      throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, 
protocols);
++    }
++    let protocolOffset = 0;
++    for (let i = 0; i < protocolIndex; i++) {
++      protocolOffset += 1 + protocols[i].length;
++    }
+
+-  return protocolOffset;
++    return protocolOffset;
++  } catch (err) {
++    socket.destroy(err);
++    return undefined;
++  }
+ }
+
+ function requestOCSP(socket, info) {
+@@ -372,63 +377,75 @@ function onnewsession(sessionId, session) {
+
+ function onPskServerCallback(identity, maxPskLen) {
+   const owner = this[owner_symbol];
+-  const ret = owner[kPskCallback](owner, identity);
+-  if (ret == null)
+-    return undefined;
+
+-  let psk;
+-  if (isArrayBufferView(ret)) {
+-    psk = ret;
+-  } else {
+-    if (typeof ret !== 'object') {
+-      throw new ERR_INVALID_ARG_TYPE(
+-        'ret',
+-        ['Object', 'Buffer', 'TypedArray', 'DataView'],
+-        ret,
++  try {
++    const ret = owner[kPskCallback](owner, identity);
++    if (ret == null)
++      return undefined;
++
++    let psk;
++    if (isArrayBufferView(ret)) {
++      psk = ret;
++    } else {
++      if (typeof ret !== 'object') {
++        throw new ERR_INVALID_ARG_TYPE(
++          'ret',
++          ['Object', 'Buffer', 'TypedArray', 'DataView'],
++          ret,
++        );
++      }
++      psk = ret.psk;
++      validateBuffer(psk, 'psk');
++    }
++
++    if (psk.length > maxPskLen) {
++      throw new ERR_INVALID_ARG_VALUE(
++        'psk',
++        psk,
++        `Pre-shared key exceeds ${maxPskLen} bytes`,
+       );
+     }
+-    psk = ret.psk;
+-    validateBuffer(psk, 'psk');
+-  }
+
+-  if (psk.length > maxPskLen) {
+-    throw new ERR_INVALID_ARG_VALUE(
+-      'psk',
+-      psk,
+-      `Pre-shared key exceeds ${maxPskLen} bytes`,
+-    );
++    return psk;
++  } catch (err) {
++    owner.destroy(err);
++    return undefined;
+   }
+-
+-  return psk;
+ }
+
+ function onPskClientCallback(hint, maxPskLen, maxIdentityLen) {
+   const owner = this[owner_symbol];
+-  const ret = owner[kPskCallback](hint);
+-  if (ret == null)
+-    return undefined;
+
+-  validateObject(ret, 'ret');
++  try {
++    const ret = owner[kPskCallback](hint);
++    if (ret == null)
++      return undefined;
++
++    validateObject(ret, 'ret');
++
++    validateBuffer(ret.psk, 'psk');
++    if (ret.psk.length > maxPskLen) {
++      throw new ERR_INVALID_ARG_VALUE(
++        'psk',
++        ret.psk,
++        `Pre-shared key exceeds ${maxPskLen} bytes`,
++      );
++    }
+
+-  validateBuffer(ret.psk, 'psk');
+-  if (ret.psk.length > maxPskLen) {
+-    throw new ERR_INVALID_ARG_VALUE(
+-      'psk',
+-      ret.psk,
+-      `Pre-shared key exceeds ${maxPskLen} bytes`,
+-    );
+-  }
++    validateString(ret.identity, 'identity');
++    if (Buffer.byteLength(ret.identity) > maxIdentityLen) {
++      throw new ERR_INVALID_ARG_VALUE(
++        'identity',
++        ret.identity,
++        `PSK identity exceeds ${maxIdentityLen} bytes`,
++      );
++    }
+
+-  validateString(ret.identity, 'identity');
+-  if (Buffer.byteLength(ret.identity) > maxIdentityLen) {
+-    throw new ERR_INVALID_ARG_VALUE(
+-      'identity',
+-      ret.identity,
+-      `PSK identity exceeds ${maxIdentityLen} bytes`,
+-    );
++    return { psk: ret.psk, identity: ret.identity };
++  } catch (err) {
++    owner.destroy(err);
++    return undefined;
+   }
+-
+-  return { psk: ret.psk, identity: ret.identity };
+ }
+
+ function onkeylog(line) {
+diff --git a/test/parallel/test-tls-alpn-server-client.js 
b/test/parallel/test-tls-alpn-server-client.js
+index 8f1a4b8e439..1ef32ca5938 100644
+--- a/test/parallel/test-tls-alpn-server-client.js
++++ b/test/parallel/test-tls-alpn-server-client.js
+@@ -252,25 +252,35 @@ function TestALPNCallback() {
+ function TestBadALPNCallback() {
+   // Server always returns a fixed invalid value:
+   const serverOptions = {
++    key: loadPEM('agent2-key'),
++    cert: loadPEM('agent2-cert'),
+     ALPNCallback: common.mustCall(() => 'http/5')
+   };
+
+-  const clientsOptions = [{
+-    ALPNProtocols: ['http/1', 'h2'],
+-  }];
++  const server = tls.createServer(serverOptions);
+
+-  process.once('uncaughtException', common.mustCall((error) => {
++  // Error should be emitted via tlsClientError, not as uncaughtException
++  server.on('tlsClientError', common.mustCall((error, socket) => {
+     assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
++    socket.destroy();
+   }));
+
+-  runTest(clientsOptions, serverOptions, function(results) {
+-    // Callback returns 'http/5' => doesn't match client ALPN => error & reset
+-    assert.strictEqual(results[0].server, undefined);
+-    const allowedErrors = ['ECONNRESET', 
'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL'];
+-    assert.ok(allowedErrors.includes(results[0].client.error.code), 
`'${results[0].client.error.code}' was not one of ${allowedErrors}.`);
++  server.listen(0, serverIP, common.mustCall(() => {
++    const client = tls.connect({
++      port: server.address().port,
++      host: serverIP,
++      rejectUnauthorized: false,
++      ALPNProtocols: ['http/1', 'h2'],
++    }, common.mustNotCall());
+
+-    TestALPNOptionsCallback();
+-  });
++    client.on('error', common.mustCall((err) => {
++      // Client gets reset when server handles error via tlsClientError
++      const allowedErrors = ['ECONNRESET', 
'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL'];
++      assert.ok(allowedErrors.includes(err.code), `'${err.code}' was not one 
of ${allowedErrors}.`);
++      server.close();
++      TestALPNOptionsCallback();
++    }));
++  }));
+ }
+
+ function TestALPNOptionsCallback() {
+diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js 
b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js
+new file mode 100644
+index 00000000000..93bf7396d2a
+--- /dev/null
++++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js
+@@ -0,0 +1,334 @@
++'use strict';
++
++// This test verifies that exceptions in pskCallback and ALPNCallback are
++// properly routed through tlsClientError instead of becoming uncaught
++// exceptions. This is a regression test for a vulnerability where callback
++// validation errors would bypass all standard TLS error handlers.
++//
++// The vulnerability allows remote attackers to crash TLS servers or cause
++// resource exhaustion (file descriptor leaks) when pskCallback or 
ALPNCallback
++// throw exceptions during validation.
++
++const common = require('../common');
++
++if (!common.hasCrypto)
++  common.skip('missing crypto');
++
++const assert = require('assert');
++const { describe, it } = require('node:test');
++const tls = require('tls');
++const fixtures = require('../common/fixtures');
++
++const CIPHERS = 'PSK+HIGH';
++const TEST_TIMEOUT = 5000;
++
++// Helper to create a promise that rejects on uncaughtException or timeout
++function createTestPromise() {
++  const { promise, resolve, reject } = Promise.withResolvers();
++  let settled = false;
++
++  const cleanup = () => {
++    if (!settled) {
++      settled = true;
++      process.removeListener('uncaughtException', onUncaught);
++      clearTimeout(timeout);
++    }
++  };
++
++  const onUncaught = (err) => {
++    cleanup();
++    reject(new Error(
++      `Uncaught exception instead of tlsClientError: ${err.code || 
err.message}`
++    ));
++  };
++
++  const timeout = setTimeout(() => {
++    cleanup();
++    reject(new Error('Test timed out - tlsClientError was not emitted'));
++  }, TEST_TIMEOUT);
++
++  process.on('uncaughtException', onUncaught);
++
++  return {
++    resolve: (value) => {
++      cleanup();
++      resolve(value);
++    },
++    reject: (err) => {
++      cleanup();
++      reject(err);
++    },
++    promise,
++  };
++}
++
++describe('TLS callback exception handling', () => {
++
++  // Test 1: PSK server callback returning invalid type should emit 
tlsClientError
++  it('pskCallback returning invalid type emits tlsClientError', async (t) => {
++    const server = tls.createServer({
++      ciphers: CIPHERS,
++      pskCallback: () => {
++        // Return invalid type (string instead of object/Buffer)
++        return 'invalid-should-be-object-or-buffer';
++      },
++      pskIdentityHint: 'test-hint',
++    });
++
++    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.code, 'ERR_INVALID_ARG_TYPE');
++        socket.destroy();
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      ciphers: CIPHERS,
++      checkServerIdentity: () => {},
++      pskCallback: () => ({
++        psk: Buffer.alloc(32),
++        identity: 'test-identity',
++      }),
++    });
++
++    client.on('error', () => {});
++
++    await promise;
++  });
++
++  // Test 2: PSK server callback throwing should emit tlsClientError
++  it('pskCallback throwing emits tlsClientError', async (t) => {
++    const server = tls.createServer({
++      ciphers: CIPHERS,
++      pskCallback: () => {
++        throw new Error('Intentional callback error');
++      },
++      pskIdentityHint: 'test-hint',
++    });
++
++    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 callback error');
++        socket.destroy();
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      ciphers: CIPHERS,
++      checkServerIdentity: () => {},
++      pskCallback: () => ({
++        psk: Buffer.alloc(32),
++        identity: 'test-identity',
++      }),
++    });
++
++    client.on('error', () => {});
++
++    await promise;
++  });
++
++  // Test 3: ALPN callback returning non-matching protocol should emit 
tlsClientError
++  it('ALPNCallback returning invalid result emits tlsClientError', async (t) 
=> {
++    const server = tls.createServer({
++      key: fixtures.readKey('agent2-key.pem'),
++      cert: fixtures.readKey('agent2-cert.pem'),
++      ALPNCallback: () => {
++        // Return a protocol not in the client's list
++        return 'invalid-protocol-not-in-list';
++      },
++    });
++
++    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.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
++        socket.destroy();
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      rejectUnauthorized: false,
++      ALPNProtocols: ['http/1.1', 'h2'],
++    });
++
++    client.on('error', () => {});
++
++    await promise;
++  });
++
++  // Test 4: ALPN callback throwing should emit tlsClientError
++  it('ALPNCallback throwing emits tlsClientError', async (t) => {
++    const server = tls.createServer({
++      key: fixtures.readKey('agent2-key.pem'),
++      cert: fixtures.readKey('agent2-cert.pem'),
++      ALPNCallback: () => {
++        throw new Error('Intentional ALPN 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 ALPN callback error');
++        socket.destroy();
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      rejectUnauthorized: false,
++      ALPNProtocols: ['http/1.1'],
++    });
++
++    client.on('error', () => {});
++
++    await promise;
++  });
++
++  // Test 5: PSK client callback returning invalid type should emit error 
event
++  it('client pskCallback returning invalid type emits error', async (t) => {
++    const PSK = Buffer.alloc(32);
++
++    const server = tls.createServer({
++      ciphers: CIPHERS,
++      pskCallback: () => PSK,
++      pskIdentityHint: 'test-hint',
++    });
++
++    t.after(() => server.close());
++
++    const { promise, resolve, reject } = createTestPromise();
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      ciphers: CIPHERS,
++      checkServerIdentity: () => {},
++      pskCallback: () => {
++        // Return invalid type - should cause validation error
++        return 'invalid-should-be-object';
++      },
++    });
++
++    client.on('error', common.mustCall((err) => {
++      try {
++        assert.ok(err instanceof Error);
++        assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    await promise;
++  });
++
++  // Test 6: PSK client callback throwing should emit error event
++  it('client pskCallback throwing emits error', async (t) => {
++    const PSK = Buffer.alloc(32);
++
++    const server = tls.createServer({
++      ciphers: CIPHERS,
++      pskCallback: () => PSK,
++      pskIdentityHint: 'test-hint',
++    });
++
++    t.after(() => server.close());
++
++    const { promise, resolve, reject } = createTestPromise();
++
++    server.on('secureConnection', common.mustNotCall(() => {
++      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',
++      ciphers: CIPHERS,
++      checkServerIdentity: () => {},
++      pskCallback: () => {
++        throw new Error('Intentional client PSK callback error');
++      },
++    });
++
++    client.on('error', common.mustCall((err) => {
++      try {
++        assert.ok(err instanceof Error);
++        assert.strictEqual(err.message, 'Intentional client PSK callback 
error');
++        resolve();
++      } catch (e) {
++        reject(e);
++      }
++    }));
++
++    await promise;
++  });
++});
+--
+2.43.7
diff --git a/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb 
b/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb
index 779c70dbd0..68eb40bc1d 100644
--- a/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb
+++ b/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb
@@ -32,6 +32,7 @@ SRC_URI = "http://nodejs.org/dist/v${PV}/node-v${PV}.tar.xz \
            file://CVE-2025-55132.patch \
            file://CVE-2025-55130.patch \
            file://CVE-2025-59466.patch \
+           file://CVE-2026-21637.patch \
            "
 SRC_URI:append:class-target = " \
            file://0001-Using-native-binaries.patch \
-- 
2.44.1

-=-=-=-=-=-=-=-=-=-=-=-
Links: You receive all messages sent to this group.
View/Reply Online (#124351): 
https://lists.openembedded.org/g/openembedded-devel/message/124351
Mute This Topic: https://lists.openembedded.org/mt/117772148/21656
Group Owner: [email protected]
Unsubscribe: https://lists.openembedded.org/g/openembedded-devel/unsub 
[[email protected]]
-=-=-=-=-=-=-=-=-=-=-=-

  • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org

Reply via email to