Skip to content

Commit df1b2c3

Browse files
tniessenkumarak
authored andcommitted
crypto,tls: implement safe x509 GeneralName format
This change introduces JSON-compatible escaping rules for strings that include X.509 GeneralName components (see RFC 5280). This non-standard format avoids ambiguities and prevents injection attacks that could previously lead to X.509 certificates being accepted even though they were not valid for the target hostname. These changes affect the format of subject alternative names and the format of authority information access. The checkServerIdentity function has been modified to safely handle the new format, eliminating the possibility of injecting subject alternative names into the verification logic. Because each subject alternative name is only encoded as a JSON string literal if necessary for security purposes, this change will only be visible in rare cases. This addresses CVE-2021-44532. Co-authored-by: Akshay K <iit.akshay@gmail.com> CVE-ID: CVE-2021-44532 Backport-PR-URL: nodejs-private/node-private#305 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent b14be42 commit df1b2c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2428
-42
lines changed

doc/api/errors.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,14 @@ An unspecified or non-specific system error has occurred within the Node.js
19751975
process. The error object will have an `err.info` object property with
19761976
additional details.
19771977

1978+
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
1979+
### `ERR_TLS_CERT_ALTNAME_FORMAT`
1980+
1981+
This error is thrown by `checkServerIdentity` if a user-supplied
1982+
`subjectaltname` property violates encoding rules. Certificate objects produced
1983+
by Node.js itself always comply with encoding rules and will never cause
1984+
this error.
1985+
19781986
<a id="ERR_TLS_CERT_ALTNAME_INVALID"></a>
19791987
### `ERR_TLS_CERT_ALTNAME_INVALID`
19801988

lib/_tls_common.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
const {
2525
ArrayIsArray,
26+
JSONParse,
2627
ObjectCreate,
2728
} = primordials;
2829

@@ -323,6 +324,14 @@ exports.translatePeerCertificate = function translatePeerCertificate(c) {
323324

324325
// XXX: More key validation?
325326
info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g, (all, key, val) => {
327+
if (val.charCodeAt(0) === 0x22) {
328+
// The translatePeerCertificate function is only
329+
// used on internally created legacy certificate
330+
// objects, and any value that contains a quote
331+
// will always be a valid JSON string literal,
332+
// so this should never throw.
333+
val = JSONParse(val);
334+
}
326335
if (key in c.infoAccess)
327336
c.infoAccess[key].push(val);
328337
else

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,8 @@ E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error);
14101410
E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error);
14111411
E('ERR_SYNTHETIC', 'JavaScript Callstack', Error);
14121412
E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError);
1413+
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
1414+
SyntaxError);
14131415
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
14141416
this.reason = reason;
14151417
this.host = host;

lib/tls.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@
2424
const {
2525
Array,
2626
ArrayIsArray,
27+
ArrayPrototypePush,
28+
JSONParse,
2729
ObjectDefineProperty,
2830
ObjectFreeze,
31+
RegExpPrototypeExec,
2932
StringFromCharCode,
3033
StringPrototypeCharCodeAt,
34+
StringPrototypeIncludes,
35+
StringPrototypeIndexOf,
3136
StringPrototypeReplace,
3237
StringPrototypeSplit,
38+
StringPrototypeSubstring,
3339
} = primordials;
3440

3541
const {
42+
ERR_TLS_CERT_ALTNAME_FORMAT,
3643
ERR_TLS_CERT_ALTNAME_INVALID,
3744
ERR_OUT_OF_RANGE
3845
} = require('internal/errors').codes;
@@ -217,6 +224,45 @@ function check(hostParts, pattern, wildcards) {
217224
return true;
218225
}
219226

227+
// This pattern is used to determine the length of escaped sequences within
228+
// the subject alt names string. It allows any valid JSON string literal.
229+
// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly.
230+
const jsonStringPattern =
231+
// eslint-disable-next-line no-control-regex
232+
/^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/;
233+
234+
function splitEscapedAltNames(altNames) {
235+
const result = [];
236+
let currentToken = '';
237+
let offset = 0;
238+
while (offset !== altNames.length) {
239+
const nextSep = StringPrototypeIndexOf(altNames, ', ', offset);
240+
const nextQuote = StringPrototypeIndexOf(altNames, '"', offset);
241+
if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) {
242+
// There is a quote character and there is no separator before the quote.
243+
currentToken += StringPrototypeSubstring(altNames, offset, nextQuote);
244+
const match = RegExpPrototypeExec(
245+
jsonStringPattern, StringPrototypeSubstring(altNames, nextQuote));
246+
if (!match) {
247+
throw new ERR_TLS_CERT_ALTNAME_FORMAT();
248+
}
249+
currentToken += JSONParse(match[0]);
250+
offset = nextQuote + match[0].length;
251+
} else if (nextSep !== -1) {
252+
// There is a separator and no quote before it.
253+
currentToken += StringPrototypeSubstring(altNames, offset, nextSep);
254+
ArrayPrototypePush(result, currentToken);
255+
currentToken = '';
256+
offset = nextSep + 2;
257+
} else {
258+
currentToken += StringPrototypeSubstring(altNames, offset);
259+
offset = altNames.length;
260+
}
261+
}
262+
ArrayPrototypePush(result, currentToken);
263+
return result;
264+
}
265+
220266
let urlWarningEmitted = false;
221267
exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
222268
const subject = cert.subject;
@@ -228,7 +274,10 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
228274
hostname = '' + hostname;
229275

230276
if (altNames) {
231-
for (const name of altNames.split(', ')) {
277+
const splitAltNames = StringPrototypeIncludes(altNames, '"') ?
278+
splitEscapedAltNames(altNames) :
279+
StringPrototypeSplit(altNames, ', ');
280+
for (const name of splitAltNames) {
232281
if (name.startsWith('DNS:')) {
233282
dnsNames.push(name.slice(4));
234283
} else if (name.startsWith('URI:')) {

0 commit comments

Comments
 (0)