Skip to content

Commit a14c158

Browse files
mcollinaaduh95
authored andcommitted
tls: fix case-sensitive SNI context matching
The regex constructed by server.addContext() lacked the case-insensitive flag, causing uppercase or mixed-case SNI hostnames from ClientHello to miss their intended context and fall back to the default context. This violates RFC 6066 Section 3, which states that DNS hostnames are case-insensitive. In mTLS configurations with per-tenant contexts, this allowed bypassing client certificate authorization by simply uppercasing the SNI hostname. Add the 'i' flag to the RegExp in addContext() so that SNI matching is case-insensitive. PR-URL: nodejs-private/node-private#857 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> CVE-ID: CVE-2026-48928 Refs: https://hackerone.com/reports/3656869
1 parent 87d847b commit a14c158

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

‎lib/internal/tls/wrap.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,7 @@ Server.prototype.addContext = function(servername, context) {
15771577
servername
15781578
.replace(/([.^$+?\-\\[\]{}])/g,'\\$1')
15791579
.replaceAll('*','[^.]*')
1580-
}$`);
1580+
}$`,'i');
15811581

15821582
constsecureContext=
15831583
contextinstanceofcommon.SecureContext ? context : tls.createSecureContext(context);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict';
2+
constcommon=require('../common');
3+
if(!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
// Test that SNI context matching is case-insensitive, as required by
7+
// RFC 6066 Section 3 and RFC 6125. Uppercase SNI hostnames must select
8+
// the same context as their lowercase equivalents.
9+
10+
constassert=require('assert');
11+
consttls=require('tls');
12+
constfixtures=require('../common/fixtures');
13+
14+
functionloadPEM(n){
15+
returnfixtures.readKey(`${n}.pem`);
16+
}
17+
18+
constserverOptions={
19+
key: loadPEM('agent2-key'),
20+
cert: loadPEM('agent2-cert'),
21+
};
22+
23+
constSNIContexts={
24+
'a.example.com': {
25+
key: loadPEM('agent1-key'),
26+
cert: loadPEM('agent1-cert'),
27+
},
28+
'*.test.com': {
29+
key: loadPEM('agent3-key'),
30+
cert: loadPEM('agent3-cert'),
31+
},
32+
};
33+
34+
consttests=[
35+
// Exact match, uppercase
36+
{servername: 'A.EXAMPLE.COM',expectedCert: 'agent1',desc: 'uppercase exact'},
37+
// Mixed case
38+
{servername: 'A.Example.Com',expectedCert: 'agent1',desc: 'mixed case exact'},
39+
// Wildcard match, uppercase
40+
{servername: 'B.TEST.COM',expectedCert: 'agent3',desc: 'uppercase wildcard'},
41+
// Wildcard match, mixed case
42+
{servername: 'b.Test.Com',expectedCert: 'agent3',desc: 'mixed case wildcard'},
43+
// Lowercase still works
44+
{servername: 'a.example.com',expectedCert: 'agent1',desc: 'lowercase exact'},
45+
];
46+
47+
letremaining=tests.length;
48+
49+
for(const{ servername, expectedCert, desc }oftests){
50+
constserver=tls.createServer(serverOptions,common.mustCall((c)=>{
51+
// The server should have selected the correct SNI context regardless of case
52+
constcert=c.getCertificate();
53+
assert.strictEqual(
54+
cert.subject.CN,expectedCert,
55+
`${desc}: expected server cert CN=${expectedCert}, got ${cert.subject.CN}`
56+
);
57+
c.end();
58+
}));
59+
60+
server.addContext('a.example.com',SNIContexts['a.example.com']);
61+
server.addContext('*.test.com',SNIContexts['*.test.com']);
62+
63+
server.listen(0,common.mustCall(()=>{
64+
constclient=tls.connect({
65+
port: server.address().port,
66+
servername,
67+
rejectUnauthorized: false,
68+
},common.mustCall(()=>{
69+
client.end();
70+
}));
71+
72+
client.on('close',common.mustCall(()=>{
73+
server.close();
74+
if(--remaining===0){
75+
process.stdout.write('all tests passed\n');
76+
}
77+
}));
78+
}));
79+
}

0 commit comments

Comments
 (0)