Skip to content

Commit 69cca87

Browse files
mcollinaaduh95
authored andcommitted
https: isolate Agent TLS policy keys
Encode TLS option types and values in HTTPS Agent keys so distinct trust and identity policies cannot share sockets or cached sessions. Assign stable identities to opaque SecureContext and callback objects. Refs: https://hackerone.com/reports/3946081 Assisted-by: codex:gpt-5.6-sol Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: nodejs-private/node-private#1036 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> CVE-ID: CVE-2026-123877
1 parent b25b496 commit 69cca87

3 files changed

Lines changed: 383 additions & 21 deletions

File tree

‎lib/https.js‎

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'use strict';
2323

2424
const {
25+
ArrayBufferIsView,
2526
ArrayIsArray,
2627
ArrayPrototypeIndexOf,
2728
ArrayPrototypePush,
@@ -35,6 +36,8 @@ const {
3536
ObjectSetPrototypeOf,
3637
ReflectApply,
3738
ReflectConstruct,
39+
SafeWeakMap,
40+
String,
3841
Symbol,
3942
SymbolAsyncDispose,
4043
} = primordials;
@@ -54,6 +57,8 @@ const {
5457
} = require('internal/tls/secure-context');
5558
const kPerRequestCheckServerIdentity = Symbol('per-request checkServerIdentity');
5659
let perRequestCheckServerIdentityIndex = 0;
60+
const tlsPolicyObjectIds = new SafeWeakMap();
61+
let tlsPolicyObjectId = 0;
5762
const {
5863
kProxyConfig,
5964
checkShouldUseProxy,
@@ -514,16 +519,65 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
514519
return FunctionPrototypeCall(HttpAgent.prototype.keepSocketAlive, this, socket);
515520
};
516521

517-
function getPfxAgentKey(pfx, passphrase) {
518-
if (!ArrayIsArray(pfx))
519-
return pfx;
522+
// Encode values with their type and length so distinct TLS policies cannot
523+
// collapse to the same Agent key. Use stable identities for opaque objects.
524+
function getTlsOptionKey(value) {
525+
if (value === undefined)
526+
return '';
527+
if (value === null)
528+
return 'n';
529+
530+
if (ArrayIsArray(value)) {
531+
let key = `a${value.length}`;
532+
for (let i = 0; i < value.length; i++) {
533+
const itemKey = getTlsOptionKey(value[i]);
534+
key += `:${itemKey.length}:${itemKey}`;
535+
}
536+
return key;
537+
}
538+
539+
if (ArrayBufferIsView(value)) {
540+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
541+
return `v${bytes.byteLength}:${bytes.toString('base64')}`;
542+
}
543+
544+
const type = typeof value;
545+
if (type === 'object' || type === 'function') {
546+
let id = tlsPolicyObjectIds.get(value);
547+
if (id === undefined) {
548+
id = ++tlsPolicyObjectId;
549+
tlsPolicyObjectIds.set(value, id);
550+
}
551+
return `o${id}`;
552+
}
553+
554+
const string = String(value);
555+
return `${type[0]}${string.length}:${string}`;
556+
}
557+
558+
function getKeyMaterialAgentKey(value, passphrase, property,
559+
useTruthyProperties) {
560+
if (!ArrayIsArray(value)) {
561+
return `${getTlsOptionKey(value)}:${getTlsOptionKey(passphrase)}`;
562+
}
520563

521-
let key = '';
522-
for (let i = 0; i < pfx.length; i++) {
523-
const value = pfx[i];
524-
const raw = value?.buf || value;
525-
const pass = value?.passphrase || passphrase;
526-
key += `:${raw}:${pass}`;
564+
let key = `a${value.length}`;
565+
for (let i = 0; i < value.length; i++) {
566+
const item = value[i];
567+
let raw = item;
568+
let pass = passphrase;
569+
if (item !== null && typeof item === 'object' &&
570+
!ArrayBufferIsView(item)) {
571+
if (useTruthyProperties) {
572+
raw = item[property] || item;
573+
pass = item.passphrase || passphrase;
574+
} else {
575+
raw = item[property] !== undefined ? item[property] : item;
576+
pass = item.passphrase !== undefined ? item.passphrase : passphrase;
577+
}
578+
}
579+
const itemKey = `${getTlsOptionKey(raw)}:${getTlsOptionKey(pass)}`;
580+
key += `:${itemKey.length}:${itemKey}`;
527581
}
528582
return key;
529583
}
@@ -544,23 +598,27 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
544598

545599
name += ':';
546600
if (options.ca)
547-
name += options.ca;
601+
name += getTlsOptionKey(options.ca);
548602

549603
name += ':';
550604
if (options.cert)
551-
name += options.cert;
605+
name += getTlsOptionKey(options.cert);
552606

553607
name += ':';
554608
if (options.ciphers)
555609
name += options.ciphers;
556610

557611
name += ':';
558-
if (options.key)
559-
name += options.key;
612+
if (options.key) {
613+
name += getKeyMaterialAgentKey(
614+
options.key, options.passphrase, 'pem', false);
615+
}
560616

561617
name += ':';
562-
if (options.pfx)
563-
name += getPfxAgentKey(options.pfx, options.passphrase);
618+
if (options.pfx) {
619+
name += getKeyMaterialAgentKey(
620+
options.pfx, options.passphrase, 'buf', true);
621+
}
564622

565623
name += ':';
566624
if (options.rejectUnauthorized !== undefined)
@@ -584,7 +642,7 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
584642

585643
name += ':';
586644
if (options.crl)
587-
name += options.crl;
645+
name += getTlsOptionKey(options.crl);
588646

589647
name += ':';
590648
if (options.honorCipherOrder !== undefined)
@@ -610,6 +668,38 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
610668
if (options.sigalgs)
611669
name += JSONStringify(options.sigalgs);
612670

671+
name += ':';
672+
if (options.secureContext)
673+
name += getTlsOptionKey(options.secureContext);
674+
675+
name += ':';
676+
if (options.allowPartialTrustChain !== undefined)
677+
name += getTlsOptionKey(options.allowPartialTrustChain);
678+
679+
name += ':';
680+
if (options.ALPNProtocols)
681+
name += getTlsOptionKey(options.ALPNProtocols);
682+
683+
name += ':';
684+
if (options.certificateCompression)
685+
name += getTlsOptionKey(options.certificateCompression);
686+
687+
name += ':';
688+
if (options.minDHSize !== undefined)
689+
name += getTlsOptionKey(options.minDHSize);
690+
691+
name += ':';
692+
if (options.pskCallback)
693+
name += getTlsOptionKey(options.pskCallback);
694+
695+
name += ':';
696+
if (options.requestOCSP !== undefined)
697+
name += getTlsOptionKey(options.requestOCSP);
698+
699+
name += ':';
700+
if (options.session)
701+
name += getTlsOptionKey(options.session);
702+
613703
if (options[kPerRequestCheckServerIdentity])
614704
name += `:${options[kPerRequestCheckServerIdentity]}`;
615705

‎test/parallel/test-https-agent-getname.js‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ if (!common.hasCrypto)
66

77
const assert = require('assert');
88
const https = require('https');
9+
const tls = require('tls');
910
const fixtures = require('../common/fixtures');
1011

1112
const agent = new https.Agent();
1213

1314
// empty argument
1415
assert.strictEqual(
1516
agent.getName(),
16-
'localhost:::::::::::::::::::'
17+
'localhost:::::::::::::::::::::::::::'
1718
);
1819

1920
// empty options
2021
assert.strictEqual(
2122
agent.getName({}),
22-
'localhost:::::::::::::::::::'
23+
'localhost:::::::::::::::::::::::::::'
2324
);
2425

2526
// Pass all options arguments
@@ -46,9 +47,10 @@ const options = {
4647

4748
assert.strictEqual(
4849
agent.getName(options),
49-
'0.0.0.0:443:192.168.1.1:ca:cert:ciphers:key:pfx:false:localhost:' +
50-
'::secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext:' +
51-
'"sigalgs"'
50+
'0.0.0.0:443:192.168.1.1:s2:ca:s4:cert:ciphers:s3:key::' +
51+
's3:pfx::false:localhost:::secureProtocol:' +
52+
'a3:7:v1:Yw==:7:v1:cg==:7:v1:bA==:false:ecdhCurve:dhparam:0:' +
53+
'sessionIdContext:"sigalgs"::::::::'
5254
);
5355

5456
{
@@ -92,4 +94,61 @@ assert.strictEqual(
9294
pfx: [{ __proto__: { buf: agent6, passphrase: 'sample' } }],
9395
})
9496
);
97+
98+
assert.notStrictEqual(
99+
agent.getName(baseOptions),
100+
agent.getName({ ...baseOptions, ca: [] })
101+
);
102+
103+
assert.notStrictEqual(
104+
agent.getName({ ...baseOptions, allowPartialTrustChain: true }),
105+
agent.getName({ ...baseOptions, allowPartialTrustChain: false })
106+
);
107+
108+
const secureContext1 = tls.createSecureContext({ ca: agent1 });
109+
const secureContext2 = tls.createSecureContext({ ca: agent6 });
110+
assert.strictEqual(
111+
agent.getName({ ...baseOptions, secureContext: secureContext1 }),
112+
agent.getName({ ...baseOptions, secureContext: secureContext1 })
113+
);
114+
assert.notStrictEqual(
115+
agent.getName({ ...baseOptions, secureContext: secureContext1 }),
116+
agent.getName({ ...baseOptions, secureContext: secureContext2 })
117+
);
118+
119+
assert.notStrictEqual(
120+
agent.getName({
121+
...baseOptions,
122+
key: [{ pem: agent1, passphrase: 'sample' }],
123+
}),
124+
agent.getName({
125+
...baseOptions,
126+
key: [{ pem: agent6, passphrase: 'sample' }],
127+
})
128+
);
129+
130+
assert.notStrictEqual(
131+
agent.getName({ ...baseOptions, ciphers: 'A:B' }),
132+
agent.getName({ ...baseOptions, ciphers: 'A', key: 'B' })
133+
);
134+
135+
assert.notStrictEqual(
136+
agent.getName({ ...baseOptions, ALPNProtocols: ['h2'] }),
137+
agent.getName({ ...baseOptions, ALPNProtocols: ['http/1.1'] })
138+
);
139+
140+
assert.notStrictEqual(
141+
agent.getName({ ...baseOptions, minDHSize: 1024 }),
142+
agent.getName({ ...baseOptions, minDHSize: 2048 })
143+
);
144+
145+
assert.notStrictEqual(
146+
agent.getName({ ...baseOptions, pskCallback() {} }),
147+
agent.getName({ ...baseOptions, pskCallback() {} })
148+
);
149+
150+
assert.notStrictEqual(
151+
agent.getName({ ...baseOptions, session: Buffer.from('one') }),
152+
agent.getName({ ...baseOptions, session: Buffer.from('two') })
153+
);
95154
}

0 commit comments

Comments
 (0)