Skip to content

Commit 466e541

Browse files
tniessenrichardlau
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. CVE-ID: CVE-2021-44532 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 26a5c58 commit 466e541

Some content is hidden

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

61 files changed

+2455
-42
lines changed

doc/api/crypto.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -2578,11 +2578,27 @@ The SHA-512 fingerprint of this certificate.
25782578

25792579
<!-- YAML
25802580
added: v15.6.0
2581+
changes:
2582+
- version: REPLACEME
2583+
pr-url: https://github.com/nodejs-private/node-private/pull/300
2584+
description: Parts of this string may be encoded as JSON string literals
2585+
in response to CVE-2021-44532.
25812586
-->
25822587

25832588
* Type: {string}
25842589

2585-
The information access content of this certificate.
2590+
A textual representation of the certificate's authority information access
2591+
extension.
2592+
2593+
This is a line feed separated list of access descriptions. Each line begins with
2594+
the access method and the kind of the access location, followed by a colon and
2595+
the value associated with the access location.
2596+
2597+
After the prefix denoting the access method and the kind of the access location,
2598+
the remainder of each line might be enclosed in quotes to indicate that the
2599+
value is a JSON string literal. For backward compatibility, Node.js only uses
2600+
JSON string literals within this property when necessary to avoid ambiguity.
2601+
Third-party code should be prepared to handle both possible entry formats.
25862602

25872603
### `x509.issuer`
25882604

@@ -2659,12 +2675,32 @@ The complete subject of this certificate.
26592675

26602676
<!-- YAML
26612677
added: v15.6.0
2678+
changes:
2679+
- version: REPLACEME
2680+
pr-url: https://github.com/nodejs-private/node-private/pull/300
2681+
description: Parts of this string may be encoded as JSON string literals
2682+
in response to CVE-2021-44532.
26622683
-->
26632684

26642685
* Type: {string}
26652686

26662687
The subject alternative name specified for this certificate.
26672688

2689+
This is a comma-separated list of subject alternative names. Each entry begins
2690+
with a string identifying the kind of the subject alternative name followed by
2691+
a colon and the value associated with the entry.
2692+
2693+
Earlier versions of Node.js incorrectly assumed that it is safe to split this
2694+
property at the two-character sequence `', '` (see [CVE-2021-44532][]). However,
2695+
both malicious and legitimate certificates can contain subject alternative names
2696+
that include this sequence when represented as a string.
2697+
2698+
After the prefix denoting the type of the entry, the remainder of each entry
2699+
might be enclosed in quotes to indicate that the value is a JSON string literal.
2700+
For backward compatibility, Node.js only uses JSON string literals within this
2701+
property when necessary to avoid ambiguity. Third-party code should be prepared
2702+
to handle both possible entry formats.
2703+
26682704
### `x509.toJSON()`
26692705

26702706
<!-- YAML
@@ -5878,6 +5914,7 @@ See the [list of SSL OP Flags][] for details.
58785914

58795915
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
58805916
[CCM mode]: #ccm-mode
5917+
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532
58815918
[Caveats]: #support-for-weak-or-compromised-algorithms
58825919
[Crypto constants]: #crypto-constants
58835920
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed

doc/api/errors.md

+9
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,15 @@ An unspecified or non-specific system error has occurred within the Node.js
25502550
process. The error object will have an `err.info` object property with
25512551
additional details.
25522552

2553+
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
2554+
2555+
### `ERR_TLS_CERT_ALTNAME_FORMAT`
2556+
2557+
This error is thrown by `checkServerIdentity` if a user-supplied
2558+
`subjectaltname` property violates encoding rules. Certificate objects produced
2559+
by Node.js itself always comply with encoding rules and will never cause
2560+
this error.
2561+
25532562
<a id="ERR_TLS_CERT_ALTNAME_INVALID"></a>
25542563

25552564
### `ERR_TLS_CERT_ALTNAME_INVALID`

lib/_tls_common.js

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const tls = require('tls');
2525

2626
const {
2727
ArrayPrototypePush,
28+
JSONParse,
2829
ObjectCreate,
2930
StringPrototypeReplace,
3031
} = primordials;
@@ -137,6 +138,14 @@ function translatePeerCertificate(c) {
137138
// XXX: More key validation?
138139
StringPrototypeReplace(info, /([^\n:]*):([^\n]*)(?:\n|$)/g,
139140
(all, key, val) => {
141+
if (val.charCodeAt(0) === 0x22) {
142+
// The translatePeerCertificate function is only
143+
// used on internally created legacy certificate
144+
// objects, and any value that contains a quote
145+
// will always be a valid JSON string literal,
146+
// so this should never throw.
147+
val = JSONParse(val);
148+
}
140149
if (key in c.infoAccess)
141150
ArrayPrototypePush(c.infoAccess[key], val);
142151
else

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,8 @@ E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error);
15251525
E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error);
15261526
E('ERR_SYNTHETIC', 'JavaScript Callstack', Error);
15271527
E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError);
1528+
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
1529+
SyntaxError);
15281530
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
15291531
this.reason = reason;
15301532
this.host = host;

lib/tls.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,25 @@ const {
3030
ArrayPrototypePush,
3131
ArrayPrototypeReduce,
3232
ArrayPrototypeSome,
33+
JSONParse,
3334
ObjectDefineProperty,
3435
ObjectFreeze,
36+
RegExpPrototypeExec,
3537
RegExpPrototypeTest,
3638
StringFromCharCode,
3739
StringPrototypeCharCodeAt,
3840
StringPrototypeEndsWith,
3941
StringPrototypeIncludes,
42+
StringPrototypeIndexOf,
4043
StringPrototypeReplace,
4144
StringPrototypeSlice,
4245
StringPrototypeSplit,
4346
StringPrototypeStartsWith,
47+
StringPrototypeSubstring,
4448
} = primordials;
4549

4650
const {
51+
ERR_TLS_CERT_ALTNAME_FORMAT,
4752
ERR_TLS_CERT_ALTNAME_INVALID,
4853
ERR_OUT_OF_RANGE
4954
} = require('internal/errors').codes;
@@ -227,6 +232,45 @@ function check(hostParts, pattern, wildcards) {
227232
return true;
228233
}
229234

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

239283
if (altNames) {
240-
const splitAltNames = StringPrototypeSplit(altNames, ', ');
284+
const splitAltNames = StringPrototypeIncludes(altNames, '"') ?
285+
splitEscapedAltNames(altNames) :
286+
StringPrototypeSplit(altNames, ', ');
241287
ArrayPrototypeForEach(splitAltNames, (name) => {
242288
if (StringPrototypeStartsWith(name, 'DNS:')) {
243289
ArrayPrototypePush(dnsNames, StringPrototypeSlice(name, 4));

0 commit comments

Comments
 (0)