镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit fa61009

Browse files
mcollinaaduh95panva
committed
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 Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> Co-authored-by: Filip Skokan <panva.ip@gmail.com> Signed-off-by: Matteo Collina <hello@matteocollina.com> Backport-PR-URL: nodejs-private/node-private#1041 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: nodejs-private/node-private#1036 CVE-ID: CVE-2026-123877
1 parent 62ae278 commit fa61009

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;
@@ -51,6 +54,8 @@ assertCrypto();
5154
const tls = require('tls');
5255
const kPerRequestCheckServerIdentity = Symbol('per-request checkServerIdentity');
5356
let perRequestCheckServerIdentityIndex = 0;
57+
const tlsPolicyObjectIds = new SafeWeakMap();
58+
let tlsPolicyObjectId = 0;
5459
const {
5560
kProxyConfig,
5661
checkShouldUseProxy,
@@ -510,16 +515,65 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
510515
return FunctionPrototypeCall(HttpAgent.prototype.keepSocketAlive, this, socket);
511516
};
512517

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

517-
let key = '';
518-
for (let i = 0; i < pfx.length; i++) {
519-
const value = pfx[i];
520-
const raw = value?.buf || value;
521-
const pass = value?.passphrase || passphrase;
522-
key += `:${raw}:${pass}`;
560+
let key = `a${value.length}`;
561+
for (let i = 0; i < value.length; i++) {
562+
const item = value[i];
563+
let raw = item;
564+
let pass = passphrase;
565+
if (item !== null && typeof item === 'object' &&
566+
!ArrayBufferIsView(item)) {
567+
if (useTruthyProperties) {
568+
raw = item[property] || item;
569+
pass = item.passphrase || passphrase;
570+
} else {
571+
raw = item[property] !== undefined ? item[property] : item;
572+
pass = item.passphrase !== undefined ? item.passphrase : passphrase;
573+
}
574+
}
575+
const itemKey = `${getTlsOptionKey(raw)}:${getTlsOptionKey(pass)}`;
576+
key += `:${itemKey.length}:${itemKey}`;
523577
}
524578
return key;
525579
}
@@ -539,11 +593,11 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
539593

540594
name += ':';
541595
if (options.ca)
542-
name += options.ca;
596+
name += getTlsOptionKey(options.ca);
543597

544598
name += ':';
545599
if (options.cert)
546-
name += options.cert;
600+
name += getTlsOptionKey(options.cert);
547601

548602
name += ':';
549603
if (options.clientCertEngine)
@@ -554,12 +608,16 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
554608
name += options.ciphers;
555609

556610
name += ':';
557-
if (options.key)
558-
name += options.key;
611+
if (options.key) {
612+
name += getKeyMaterialAgentKey(
613+
options.key, options.passphrase, 'pem', false);
614+
}
559615

560616
name += ':';
561-
if (options.pfx)
562-
name += getPfxAgentKey(options.pfx, options.passphrase);
617+
if (options.pfx) {
618+
name += getKeyMaterialAgentKey(
619+
options.pfx, options.passphrase, 'buf', true);
620+
}
563621

564622
name += ':';
565623
if (options.rejectUnauthorized !== undefined)
@@ -583,7 +641,7 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
583641

584642
name += ':';
585643
if (options.crl)
586-
name += options.crl;
644+
name += getTlsOptionKey(options.crl);
587645

588646
name += ':';
589647
if (options.honorCipherOrder !== undefined)
@@ -617,6 +675,38 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
617675
if (options.privateKeyEngine)
618676
name += options.privateKeyEngine;
619677

678+
name += ':';
679+
if (options.secureContext)
680+
name += getTlsOptionKey(options.secureContext);
681+
682+
name += ':';
683+
if (options.allowPartialTrustChain !== undefined)
684+
name += getTlsOptionKey(options.allowPartialTrustChain);
685+
686+
name += ':';
687+
if (options.ALPNProtocols)
688+
name += getTlsOptionKey(options.ALPNProtocols);
689+
690+
name += ':';
691+
if (options.certificateCompression)
692+
name += getTlsOptionKey(options.certificateCompression);
693+
694+
name += ':';
695+
if (options.minDHSize !== undefined)
696+
name += getTlsOptionKey(options.minDHSize);
697+
698+
name += ':';
699+
if (options.pskCallback)
700+
name += getTlsOptionKey(options.pskCallback);
701+
702+
name += ':';
703+
if (options.requestOCSP !== undefined)
704+
name += getTlsOptionKey(options.requestOCSP);
705+
706+
name += ':';
707+
if (options.session)
708+
name += getTlsOptionKey(options.session);
709+
620710
if (options[kPerRequestCheckServerIdentity])
621711
name += `:${options[kPerRequestCheckServerIdentity]}`;
622712

‎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
@@ -49,9 +50,10 @@ const options = {
4950

5051
assert.strictEqual(
5152
agent.getName(options),
52-
'0.0.0.0:443:192.168.1.1:ca:cert:dynamic:ciphers:key:pfx:false:localhost:' +
53-
'::secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext:' +
54-
'"sigalgs":privateKeyIdentifier:privateKeyEngine'
53+
'0.0.0.0:443:192.168.1.1:s2:ca:s4:cert:dynamic:ciphers:s3:key::' +
54+
's3:pfx::false:localhost:::secureProtocol:' +
55+
'a3:7:v1:Yw==:7:v1:cg==:7:v1:bA==:false:ecdhCurve:dhparam:0:' +
56+
'sessionIdContext:"sigalgs":privateKeyIdentifier:privateKeyEngine::::::::'
5557
);
5658

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

0 commit comments

Comments
 (0)