Skip to content

Commit 0d7e21e

Browse files
bnoordhuisrvagg
authored andcommitted
lib: make tls.checkServerIdentity() more strict
Incorporates changes from commit e345253 ("tls: better error reporting at cert validation") to test/simple/test-tls-check-server-identity.js to make back-porting the patch easier. CVE-2016-7099 PR-URL: nodejs-private/node-private#62 Reviewed-By: Rod Vagg <rod@vagg.org>
1 parent 3614a17 commit 0d7e21e

File tree

2 files changed

+248
-138
lines changed

2 files changed

+248
-138
lines changed

lib/tls.js

Lines changed: 141 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -96,115 +96,163 @@ function convertNPNProtocols(NPNProtocols, out) {
9696
}
9797
}
9898

99+
function unfqdn(host) {
100+
return host.replace(/[.]$/, '');
101+
}
99102

100-
function checkServerIdentity(host, cert) {
101-
// Create regexp to much hostnames
102-
function regexpify(host, wildcards) {
103-
// Add trailing dot (make hostnames uniform)
104-
if (!/\.$/.test(host)) host += '.';
105-
106-
// The same applies to hostname with more than one wildcard,
107-
// if hostname has wildcard when wildcards are not allowed,
108-
// or if there are less than two dots after wildcard (i.e. *.com or *d.com)
109-
//
110-
// also
111-
//
112-
// "The client SHOULD NOT attempt to match a presented identifier in
113-
// which the wildcard character comprises a label other than the
114-
// left-most label (e.g., do not match bar.*.example.net)."
115-
// RFC6125
116-
if (!wildcards && /\*/.test(host) || /[\.\*].*\*/.test(host) ||
117-
/\*/.test(host) && !/\*.*\..+\..+/.test(host)) {
118-
return /$./;
119-
}
103+
function splitHost(host) {
104+
// String#toLowerCase() is locale-sensitive so we use
105+
// a conservative version that only lowercases A-Z.
106+
function replacer(c) {
107+
return String.fromCharCode(32 + c.charCodeAt(0));
108+
};
109+
return unfqdn(host).replace(/[A-Z]/g, replacer).split('.');
110+
}
120111

121-
// Replace wildcard chars with regexp's wildcard and
122-
// escape all characters that have special meaning in regexps
123-
// (i.e. '.', '[', '{', '*', and others)
124-
var re = host.replace(
125-
/\*([a-z0-9\\-_\.])|[\.,\-\\\^\$+?*\[\]\(\):!\|{}]/g,
126-
function(all, sub) {
127-
if (sub) return '[a-z0-9\\-_]*' + (sub === '-' ? '\\-' : sub);
128-
return '\\' + all;
129-
});
130-
131-
return new RegExp('^' + re + '$', 'i');
112+
function check(hostParts, pattern, wildcards) {
113+
// Empty strings, null, undefined, etc. never match.
114+
if (!pattern)
115+
return false;
116+
117+
var patternParts = splitHost(pattern);
118+
119+
if (hostParts.length !== patternParts.length)
120+
return false;
121+
122+
// Pattern has empty components, e.g. "bad..example.com".
123+
if (patternParts.indexOf('') !== -1)
124+
return false;
125+
126+
// RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
127+
// good way to detect their encoding or normalize them so we simply
128+
// reject them. Control characters and blanks are rejected as well
129+
// because nothing good can come from accepting them.
130+
function isBad(s) {
131+
return /[^\u0021-\u007F]/.test(s);
132132
}
133133

134-
var dnsNames = [],
135-
uriNames = [],
136-
ips = [],
137-
matchCN = true,
138-
valid = false;
134+
if (patternParts.some(isBad))
135+
return false;
139136

140-
// There're several names to perform check against:
141-
// CN and altnames in certificate extension
142-
// (DNS names, IP addresses, and URIs)
143-
//
144-
// Walk through altnames and generate lists of those names
145-
if (cert.subjectaltname) {
146-
cert.subjectaltname.split(/, /g).forEach(function(altname) {
147-
if (/^DNS:/.test(altname)) {
148-
dnsNames.push(altname.slice(4));
149-
} else if (/^IP Address:/.test(altname)) {
150-
ips.push(altname.slice(11));
151-
} else if (/^URI:/.test(altname)) {
152-
var uri = url.parse(altname.slice(4));
153-
if (uri) uriNames.push(uri.hostname);
137+
// Check host parts from right to left first.
138+
for (var i = hostParts.length - 1; i > 0; i -= 1)
139+
if (hostParts[i] !== patternParts[i])
140+
return false;
141+
142+
var hostSubdomain = hostParts[0];
143+
var patternSubdomain = patternParts[0];
144+
var patternSubdomainParts = patternSubdomain.split('*');
145+
146+
// Short-circuit when the subdomain does not contain a wildcard.
147+
// RFC 6125 does not allow wildcard substitution for components
148+
// containing IDNA A-labels (Punycode) so match those verbatim.
149+
if (patternSubdomainParts.length === 1 ||
150+
patternSubdomain.indexOf('xn--') !== -1) {
151+
return hostSubdomain === patternSubdomain;
152+
}
153+
154+
if (!wildcards)
155+
return false;
156+
157+
// More than one wildcard is always wrong.
158+
if (patternSubdomainParts.length > 2)
159+
return false;
160+
161+
// *.tld wildcards are not allowed.
162+
if (patternParts.length <= 2)
163+
return false;
164+
165+
var prefix = patternSubdomainParts[0];
166+
var suffix = patternSubdomainParts[1];
167+
168+
if (prefix.length + suffix.length > hostSubdomain.length)
169+
return false;
170+
171+
if (prefix.length > 0 && hostSubdomain.slice(0, prefix.length) !== prefix)
172+
return false;
173+
174+
if (suffix.length > 0 && hostSubdomain.slice(-suffix.length) !== suffix)
175+
return false;
176+
177+
return true;
178+
}
179+
180+
function _checkServerIdentity(host, cert) {
181+
var subject = cert.subject;
182+
var altNames = cert.subjectaltname;
183+
var dnsNames = [];
184+
var uriNames = [];
185+
var ips = [];
186+
187+
host = '' + host;
188+
189+
if (altNames) {
190+
altNames.split(', ').forEach(function(name) {
191+
if (/^DNS:/.test(name)) {
192+
dnsNames.push(name.slice(4));
193+
} else if (/^URI:/.test(name)) {
194+
var uri = url.parse(name.slice(4));
195+
uriNames.push(uri.hostname); // TODO(bnoordhuis) Also use scheme.
196+
} else if (/^IP Address:/.test(name)) {
197+
ips.push(name.slice(11));
154198
}
155199
});
156200
}
157201

158-
// If hostname is an IP address, it should be present in the list of IP
159-
// addresses.
202+
var valid = false;
203+
var reason = 'Unknown reason';
204+
160205
if (net.isIP(host)) {
161-
valid = ips.some(function(ip) {
162-
return ip === host;
163-
});
164-
} else {
165-
// Transform hostname to canonical form
166-
if (!/\.$/.test(host)) host += '.';
206+
valid = ips.indexOf(host) !== -1;
207+
if (!valid)
208+
reason = 'IP: ' + host + ' is not in the cert\'s list: ' + ips.join(', ');
209+
// TODO(bnoordhuis) Also check URI SANs that are IP addresses.
210+
} else if (subject) {
211+
host = unfqdn(host); // Remove trailing dot for error messages.
212+
var hostParts = splitHost(host);
213+
214+
function wildcard(pattern) {
215+
return check(hostParts, pattern, true);
216+
}
167217

168-
// Otherwise check all DNS/URI records from certificate
169-
// (with allowed wildcards)
170-
dnsNames = dnsNames.map(function(name) {
171-
return regexpify(name, true);
172-
});
218+
function noWildcard(pattern) {
219+
return check(hostParts, pattern, false);
220+
}
173221

174-
// Wildcards ain't allowed in URI names
175-
uriNames = uriNames.map(function(name) {
176-
return regexpify(name, false);
177-
});
222+
// Match against Common Name only if no supported identifiers are present.
223+
if (dnsNames.length === 0 && ips.length === 0 && uriNames.length === 0) {
224+
var cn = subject.CN;
178225

179-
dnsNames = dnsNames.concat(uriNames);
180-
181-
if (dnsNames.length > 0) matchCN = false;
182-
183-
// Match against Common Name (CN) only if no supported identifiers are
184-
// present.
185-
//
186-
// "As noted, a client MUST NOT seek a match for a reference identifier
187-
// of CN-ID if the presented identifiers include a DNS-ID, SRV-ID,
188-
// URI-ID, or any application-specific identifier types supported by the
189-
// client."
190-
// RFC6125
191-
if (matchCN) {
192-
var commonNames = cert.subject.CN;
193-
if (Array.isArray(commonNames)) {
194-
for (var i = 0, k = commonNames.length; i < k; ++i) {
195-
dnsNames.push(regexpify(commonNames[i], true));
196-
}
197-
} else {
198-
dnsNames.push(regexpify(commonNames, true));
226+
if (Array.isArray(cn))
227+
valid = cn.some(wildcard);
228+
else if (cn)
229+
valid = wildcard(cn);
230+
231+
if (!valid)
232+
reason = 'Host: ' + host + '. is not cert\'s CN: ' + cn;
233+
} else {
234+
valid = dnsNames.some(wildcard) || uriNames.some(noWildcard);
235+
if (!valid) {
236+
reason =
237+
'Host: ' + host + '. is not in the cert\'s altnames: ' + altNames;
199238
}
200239
}
240+
} else {
241+
reason = 'Cert is empty';
242+
}
201243

202-
valid = dnsNames.some(function(re) {
203-
return re.test(host);
204-
});
244+
if (!valid) {
245+
var err = new Error('Hostname/IP doesn\'t match certificate\'s altnames');
246+
err.reason = reason;
247+
err.host = host;
248+
err.cert = cert;
249+
return err;
205250
}
251+
}
252+
exports._checkServerIdentity = _checkServerIdentity;
206253

207-
return valid;
254+
function checkServerIdentity(host, cert) {
255+
return !!_checkServerIdentity(host, cert);
208256
}
209257
exports.checkServerIdentity = checkServerIdentity;
210258

@@ -1384,12 +1432,8 @@ exports.connect = function(/* [port, host], options, cb */) {
13841432

13851433
// Verify that server's identity matches it's certificate's names
13861434
if (!verifyError) {
1387-
var validCert = checkServerIdentity(hostname,
1388-
pair.cleartext.getPeerCertificate());
1389-
if (!validCert) {
1390-
verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' +
1391-
'altnames');
1392-
}
1435+
verifyError = _checkServerIdentity(hostname,
1436+
pair.cleartext.getPeerCertificate());
13931437
}
13941438

13951439
if (verifyError) {

0 commit comments

Comments
 (0)