From cc6967f7ff785a9d69ec6d3bd0c777288f6f9164 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 20 Jul 2026 13:15:10 -0300 Subject: [PATCH] https: distinguish PFX object-array agent keys Signed-off-by: RafaelGSS PR-URL: https://github.com/nodejs-private/node-private/pull/930 Refs: https://hackerone.com/reports/3816840 CVE-ID: CVE-2026-56850 bug: https://nodejs.org/en/blog/vulnerability/july-2026-security-releases#https-agent-can-reuse-mtls-identities-across-pfx-certificates-cve-2026-56850---medium origin: https://github.com/nodejs/node/commit/acaf4266b2be7958e3d7cb44b5ee1c2b96eca278 Gbp-Pq: Topic sec Gbp-Pq: Name CVE-2026-56850.patch --- lib/https.js | 17 +++++- test/parallel/test-https-agent-getname.js | 44 ++++++++++++++ ...test-https-agent-pfx-object-array-reuse.js | 59 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-https-agent-pfx-object-array-reuse.js diff --git a/lib/https.js b/lib/https.js index e74dbcf43..818f95d5a 100644 --- a/lib/https.js +++ b/lib/https.js @@ -22,6 +22,7 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypeShift, @@ -208,6 +209,20 @@ ObjectSetPrototypeOf(Agent.prototype, HttpAgent.prototype); ObjectSetPrototypeOf(Agent, HttpAgent); Agent.prototype.createConnection = createConnection; +function getPfxAgentKey(pfx, passphrase) { + if (!ArrayIsArray(pfx)) + return pfx; + + let key = ''; + for (let i = 0; i < pfx.length; i++) { + const value = pfx[i]; + const raw = value?.buf || value; + const pass = value?.passphrase || passphrase; + key += `:${raw}:${pass}`; + } + return key; +} + /** * Gets a unique name for a set of options. * @param {{ @@ -243,7 +258,7 @@ Agent.prototype.getName = function getName(options = kEmptyObject) { name += ':'; if (options.pfx) - name += options.pfx; + name += getPfxAgentKey(options.pfx, options.passphrase); name += ':'; if (options.rejectUnauthorized !== undefined) diff --git a/test/parallel/test-https-agent-getname.js b/test/parallel/test-https-agent-getname.js index 2a13ab1c6..8ead852b1 100644 --- a/test/parallel/test-https-agent-getname.js +++ b/test/parallel/test-https-agent-getname.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const https = require('https'); +const fixtures = require('../common/fixtures'); const agent = new https.Agent(); @@ -52,3 +53,46 @@ assert.strictEqual( '::secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext:' + '"sigalgs":privateKeyIdentifier:privateKeyEngine' ); + +{ + const baseOptions = { + host: '0.0.0.0', + port: 443, + }; + + const agent1 = fixtures.readKey('agent1.pfx'); + const agent6 = fixtures.readKey('agent6.pfx'); + + assert.notStrictEqual( + agent.getName({ + ...baseOptions, + pfx: [{ buf: agent1, passphrase: 'sample' }], + }), + agent.getName({ + ...baseOptions, + pfx: [{ buf: agent6, passphrase: 'sample' }], + }) + ); + + assert.notStrictEqual( + agent.getName({ + ...baseOptions, + pfx: [{ buf: agent1, passphrase: 'sample' }], + }), + agent.getName({ + ...baseOptions, + pfx: [{ buf: agent1, passphrase: 'different' }], + }) + ); + + assert.notStrictEqual( + agent.getName({ + ...baseOptions, + pfx: [{ __proto__: { buf: agent1, passphrase: 'sample' } }], + }), + agent.getName({ + ...baseOptions, + pfx: [{ __proto__: { buf: agent6, passphrase: 'sample' } }], + }) + ); +} diff --git a/test/parallel/test-https-agent-pfx-object-array-reuse.js b/test/parallel/test-https-agent-pfx-object-array-reuse.js new file mode 100644 index 000000000..95134855e --- /dev/null +++ b/test/parallel/test-https-agent-pfx-object-array-reuse.js @@ -0,0 +1,59 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const https = require('https'); +const fixtures = require('../common/fixtures'); + +const server = https.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + requestCert: true, + rejectUnauthorized: false, +}, common.mustCall((req, res) => { + res.end(req.socket.getPeerCertificate().subject.CN); +}, 2)); + +server.listen(0, common.mustCall(async () => { + const agent = new https.Agent({ keepAlive: true, maxSockets: 1 }); + const port = server.address().port; + + const first = await request({ + agent, + port, + pfx: [{ buf: fixtures.readKey('agent1.pfx'), passphrase: 'sample' }], + }); + assert.strictEqual(first.body, 'agent1'); + assert.strictEqual(first.reusedSocket, false); + + const second = await request({ + agent, + port, + pfx: [{ buf: fixtures.readKey('agent10.pfx'), passphrase: 'sample' }], + }); + assert.strictEqual(second.body, 'agent10.example.com'); + assert.strictEqual(second.reusedSocket, false); + + agent.destroy(); + server.close(); +})); + +function request(options) { + return new Promise((resolve, reject) => { + const req = https.get({ + ...options, + rejectUnauthorized: false, + }, common.mustCall((res) => { + let body = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => body += chunk); + res.on('end', common.mustCall(() => { + resolve({ body, reusedSocket: req.reusedSocket }); + })); + })); + req.on('error', reject); + }); +} -- 2.39.5