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 <[email protected]> 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
const secureContext =
15831583
context instanceof common.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+
const common = 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+
const assert = require('assert');
11+
const tls = require('tls');
12+
const fixtures = require('../common/fixtures');
13+
14+
function loadPEM(n) {
15+
return fixtures.readKey(`${n}.pem`);
16+
}
17+
18+
const serverOptions = {
19+
key: loadPEM('agent2-key'),
20+
cert: loadPEM('agent2-cert'),
21+
};
22+
23+
const SNIContexts = {
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+
const tests = [
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+
let remaining = tests.length;
48+
49+
for (const { servername, expectedCert, desc } of tests) {
50+
const server = tls.createServer(serverOptions, common.mustCall((c) => {
51+
// The server should have selected the correct SNI context regardless of case
52+
const cert = 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+
const client = 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)