Skip to content

Commit e52882d

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#304 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 4a262d4 commit e52882d

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2565,11 +2565,27 @@ The SHA-256 fingerprint of this certificate.
25652565

25662566
<!-- YAML
25672567
added: v15.6.0
2568+
changes:
2569+
- version: REPLACEME
2570+
pr-url: https://github.com/nodejs-private/node-private/pull/300
2571+
description: Parts of this string may be encoded as JSON string literals
2572+
in response to CVE-2021-44532.
25682573
-->
25692574

25702575
* Type: {string}
25712576

2572-
The information access content of this certificate.
2577+
A textual representation of the certificate's authority information access
2578+
extension.
2579+
2580+
This is a line feed separated list of access descriptions. Each line begins with
2581+
the access method and the kind of the access location, followed by a colon and
2582+
the value associated with the access location.
2583+
2584+
After the prefix denoting the access method and the kind of the access location,
2585+
the remainder of each line might be enclosed in quotes to indicate that the
2586+
value is a JSON string literal. For backward compatibility, Node.js only uses
2587+
JSON string literals within this property when necessary to avoid ambiguity.
2588+
Third-party code should be prepared to handle both possible entry formats.
25732589

25742590
### `x509.issuer`
25752591

@@ -2646,12 +2662,32 @@ The complete subject of this certificate.
26462662

26472663
<!-- YAML
26482664
added: v15.6.0
2665+
changes:
2666+
- version: REPLACEME
2667+
pr-url: https://github.com/nodejs-private/node-private/pull/300
2668+
description: Parts of this string may be encoded as JSON string literals
2669+
in response to CVE-2021-44532.
26492670
-->
26502671

26512672
* Type: {string}
26522673

26532674
The subject alternative name specified for this certificate.
26542675

2676+
This is a comma-separated list of subject alternative names. Each entry begins
2677+
with a string identifying the kind of the subject alternative name followed by
2678+
a colon and the value associated with the entry.
2679+
2680+
Earlier versions of Node.js incorrectly assumed that it is safe to split this
2681+
property at the two-character sequence `', '` (see [CVE-2021-44532][]). However,
2682+
both malicious and legitimate certificates can contain subject alternative names
2683+
that include this sequence when represented as a string.
2684+
2685+
After the prefix denoting the type of the entry, the remainder of each entry
2686+
might be enclosed in quotes to indicate that the value is a JSON string literal.
2687+
For backward compatibility, Node.js only uses JSON string literals within this
2688+
property when necessary to avoid ambiguity. Third-party code should be prepared
2689+
to handle both possible entry formats.
2690+
26552691
### `x509.toJSON()`
26562692

26572693
<!-- YAML
@@ -5838,6 +5874,7 @@ See the [list of SSL OP Flags][] for details.
58385874

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

doc/api/errors.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2505,6 +2505,15 @@ An unspecified or non-specific system error has occurred within the Node.js
25052505
process. The error object will have an `err.info` object property with
25062506
additional details.
25072507

2508+
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
2509+
2510+
### `ERR_TLS_CERT_ALTNAME_FORMAT`
2511+
2512+
This error is thrown by `checkServerIdentity` if a user-supplied
2513+
`subjectaltname` property violates encoding rules. Certificate objects produced
2514+
by Node.js itself always comply with encoding rules and will never cause
2515+
this error.
2516+
25082517
<a id="ERR_TLS_CERT_ALTNAME_INVALID"></a>
25092518

25102519
### `ERR_TLS_CERT_ALTNAME_INVALID`

lib/_tls_common.js

Lines changed: 9 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,8 @@ E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error);
15161516
E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error);
15171517
E('ERR_SYNTHETIC', 'JavaScript Callstack', Error);
15181518
E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError);
1519+
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
1520+
SyntaxError);
15191521
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
15201522
this.reason = reason;
15211523
this.host = host;

lib/tls.js

Lines changed: 47 additions & 1 deletion
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)