Skip to content

Commit a336444

Browse files
tniessenrichardlau
authored andcommitted
tls: fix handling of x509 subject and issuer
When subject and verifier are represented as strings, escape special characters (such as '+') to guarantee unambiguity. Previously, different distinguished names could result in the same string when encoded. In particular, inserting a '+' in a single-value Relative Distinguished Name (e.g., L or OU) would produce a string that is indistinguishable from a multi-value Relative Distinguished Name. Third-party code that correctly interprets the generated string representation as a multi-value Relative Distinguished Name could then be vulnerable to an injection attack, e.g., when an attacker includes a single-value RDN with type OU and value 'HR + CN=example.com', the string representation produced by unpatched versions of Node.js would be 'OU=HR + CN=example.com', which represents a multi-value RDN. Node.js itself is not vulnerable to this attack because the current implementation that parses such strings into objects does not handle '+' at all. This oversight leads to incorrect results, but at the same time appears to prevent injection attacks (as described above). With this change, the JavaScript objects representing the subject and issuer Relative Distinguished Names are constructed in C++ directly, instead of (incorrectly) encoding them as strings and then (incorrectly) decoding the strings in JavaScript. This addresses CVE-2021-44533. CVE-ID: CVE-2021-44533 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 50439b4 commit a336444

16 files changed

+656
-13
lines changed

lib/_tls_common.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,13 @@ function translatePeerCertificate(c) {
126126
if (!c)
127127
return null;
128128

129-
if (c.issuer != null) c.issuer = parseCertString(c.issuer);
129+
// TODO(tniessen): can we remove parseCertString without breaking anything?
130+
if (typeof c.issuer === 'string') c.issuer = parseCertString(c.issuer);
130131
if (c.issuerCertificate != null && c.issuerCertificate !== c) {
131132
c.issuerCertificate = translatePeerCertificate(c.issuerCertificate);
132133
}
133-
if (c.subject != null) c.subject = parseCertString(c.subject);
134+
// TODO(tniessen): can we remove parseCertString without breaking anything?
135+
if (typeof c.subject === 'string') c.subject = parseCertString(c.subject);
134136
if (c.infoAccess != null) {
135137
const info = c.infoAccess;
136138
c.infoAccess = ObjectCreate(null);

src/crypto/crypto_common.cc

+119-9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ using v8::Value;
4242

4343
namespace crypto {
4444
static constexpr int X509_NAME_FLAGS =
45+
ASN1_STRFLGS_ESC_2253 |
4546
ASN1_STRFLGS_ESC_CTRL |
4647
ASN1_STRFLGS_UTF8_CONVERT |
4748
XN_FLAG_SEP_MULTILINE |
@@ -964,6 +965,93 @@ MaybeLocal<Value> GetSubject(
964965
return ToV8Value(env, bio);
965966
}
966967

968+
template <X509_NAME* get_name(const X509*)>
969+
static MaybeLocal<Value> GetX509NameObject(Environment* env, X509* cert) {
970+
X509_NAME* name = get_name(cert);
971+
CHECK_NOT_NULL(name);
972+
973+
int cnt = X509_NAME_entry_count(name);
974+
CHECK_GE(cnt, 0);
975+
976+
Local<Object> result =
977+
Object::New(env->isolate(), Null(env->isolate()), nullptr, nullptr, 0);
978+
if (result.IsEmpty()) {
979+
return MaybeLocal<Value>();
980+
}
981+
982+
for (int i = 0; i < cnt; i++) {
983+
X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i);
984+
CHECK_NOT_NULL(entry);
985+
986+
// We intentionally ignore the value of X509_NAME_ENTRY_set because the
987+
// representation as an object does not allow grouping entries into sets
988+
// anyway, and multi-value RDNs are rare, i.e., the vast majority of
989+
// Relative Distinguished Names contains a single type-value pair only.
990+
const ASN1_OBJECT* type = X509_NAME_ENTRY_get_object(entry);
991+
const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);
992+
993+
// If OpenSSL knows the type, use the short name of the type as the key, and
994+
// the numeric representation of the type's OID otherwise.
995+
int type_nid = OBJ_obj2nid(type);
996+
char type_buf[80];
997+
const char* type_str;
998+
if (type_nid != NID_undef) {
999+
type_str = OBJ_nid2sn(type_nid);
1000+
CHECK_NOT_NULL(type_str);
1001+
} else {
1002+
OBJ_obj2txt(type_buf, sizeof(type_buf), type, true);
1003+
type_str = type_buf;
1004+
}
1005+
1006+
Local<String> v8_name;
1007+
if (!String::NewFromUtf8(env->isolate(), type_str).ToLocal(&v8_name)) {
1008+
return MaybeLocal<Value>();
1009+
}
1010+
1011+
// The previous implementation used X509_NAME_print_ex, which escapes some
1012+
// characters in the value. The old implementation did not decode/unescape
1013+
// values correctly though, leading to ambiguous and incorrect
1014+
// representations. The new implementation only converts to Unicode and does
1015+
// not escape anything.
1016+
unsigned char* value_str;
1017+
int value_str_size = ASN1_STRING_to_UTF8(&value_str, value);
1018+
if (value_str_size < 0) {
1019+
return Undefined(env->isolate());
1020+
}
1021+
1022+
Local<String> v8_value;
1023+
if (!String::NewFromUtf8(env->isolate(),
1024+
reinterpret_cast<const char*>(value_str),
1025+
NewStringType::kNormal,
1026+
value_str_size).ToLocal(&v8_value)) {
1027+
OPENSSL_free(value_str);
1028+
return MaybeLocal<Value>();
1029+
}
1030+
1031+
OPENSSL_free(value_str);
1032+
1033+
// For backward compatibility, we only create arrays if multiple values
1034+
// exist for the same key. That is not great but there is not much we can
1035+
// change here without breaking things. Note that this creates nested data
1036+
// structures, yet still does not allow representing Distinguished Names
1037+
// accurately.
1038+
if (result->HasOwnProperty(env->context(), v8_name).ToChecked()) {
1039+
Local<Value> accum =
1040+
result->Get(env->context(), v8_name).ToLocalChecked();
1041+
if (!accum->IsArray()) {
1042+
accum = Array::New(env->isolate(), &accum, 1);
1043+
result->Set(env->context(), v8_name, accum).Check();
1044+
}
1045+
Local<Array> array = accum.As<Array>();
1046+
array->Set(env->context(), array->Length(), v8_value).Check();
1047+
} else {
1048+
result->Set(env->context(), v8_name, v8_value).Check();
1049+
}
1050+
}
1051+
1052+
return result;
1053+
}
1054+
9671055
MaybeLocal<Value> GetCipherName(Environment* env, const SSLPointer& ssl) {
9681056
return GetCipherName(env, SSL_get_current_cipher(ssl.get()));
9691057
}
@@ -1194,22 +1282,44 @@ MaybeLocal<Value> GetPeerCert(
11941282
return result;
11951283
}
11961284

1197-
MaybeLocal<Object> X509ToObject(Environment* env, X509* cert) {
1285+
MaybeLocal<Object> X509ToObject(
1286+
Environment* env,
1287+
X509* cert,
1288+
bool names_as_string) {
11981289
EscapableHandleScope scope(env->isolate());
11991290
Local<Context> context = env->context();
12001291
Local<Object> info = Object::New(env->isolate());
12011292

12021293
BIOPointer bio(BIO_new(BIO_s_mem()));
12031294

1295+
if (names_as_string) {
1296+
// TODO(tniessen): this branch should not have to exist. It is only here
1297+
// because toLegacyObject() does not actually return a legacy object, and
1298+
// instead represents subject and issuer as strings.
1299+
if (!Set<Value>(context,
1300+
info,
1301+
env->subject_string(),
1302+
GetSubject(env, bio, cert)) ||
1303+
!Set<Value>(context,
1304+
info,
1305+
env->issuer_string(),
1306+
GetIssuerString(env, bio, cert))) {
1307+
return MaybeLocal<Object>();
1308+
}
1309+
} else {
1310+
if (!Set<Value>(context,
1311+
info,
1312+
env->subject_string(),
1313+
GetX509NameObject<X509_get_subject_name>(env, cert)) ||
1314+
!Set<Value>(context,
1315+
info,
1316+
env->issuer_string(),
1317+
GetX509NameObject<X509_get_issuer_name>(env, cert))) {
1318+
return MaybeLocal<Object>();
1319+
}
1320+
}
1321+
12041322
if (!Set<Value>(context,
1205-
info,
1206-
env->subject_string(),
1207-
GetSubject(env, bio, cert)) ||
1208-
!Set<Value>(context,
1209-
info,
1210-
env->issuer_string(),
1211-
GetIssuerString(env, bio, cert)) ||
1212-
!Set<Value>(context,
12131323
info,
12141324
env->subjectaltname_string(),
12151325
GetSubjectAltNameString(env, bio, cert)) ||

src/crypto/crypto_common.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ v8::MaybeLocal<v8::Object> ECPointToBuffer(
116116

117117
v8::MaybeLocal<v8::Object> X509ToObject(
118118
Environment* env,
119-
X509* cert);
119+
X509* cert,
120+
bool names_as_string = false);
120121

121122
v8::MaybeLocal<v8::Value> GetValidTo(
122123
Environment* env,

src/crypto/crypto_x509.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ void X509Certificate::ToLegacy(const FunctionCallbackInfo<Value>& args) {
470470
X509Certificate* cert;
471471
ASSIGN_OR_RETURN_UNWRAP(&cert, args.Holder());
472472
Local<Value> ret;
473-
if (X509ToObject(env, cert->get()).ToLocal(&ret))
473+
if (X509ToObject(env, cert->get(), true).ToLocal(&ret))
474474
args.GetReturnValue().Set(ret);
475475
}
476476

test/fixtures/x509-escaping/create-certs.js

+141
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,144 @@ for (let i = 0; i < infoAccessExtensions.length; i++) {
500500
});
501501
writeFileSync(`./info-${i}-cert.pem`, `${pem}\n`);
502502
}
503+
504+
const subjects = [
505+
[
506+
[
507+
{ type: oid.localityName, value: UTF8String.encode('Somewhere') }
508+
],
509+
[
510+
{ type: oid.commonName, value: UTF8String.encode('evil.example.com') }
511+
]
512+
],
513+
[
514+
[
515+
{
516+
type: oid.localityName,
517+
value: UTF8String.encode('Somewhere\0evil.example.com'),
518+
}
519+
]
520+
],
521+
[
522+
[
523+
{
524+
type: oid.localityName,
525+
value: UTF8String.encode('Somewhere\nCN=evil.example.com')
526+
}
527+
]
528+
],
529+
[
530+
[
531+
{
532+
type: oid.localityName,
533+
value: UTF8String.encode('Somewhere, CN = evil.example.com')
534+
}
535+
]
536+
],
537+
[
538+
[
539+
{
540+
type: oid.localityName,
541+
value: UTF8String.encode('Somewhere/CN=evil.example.com')
542+
}
543+
]
544+
],
545+
[
546+
[
547+
{
548+
type: oid.localityName,
549+
value: UTF8String.encode('M\u00fcnchen\\\nCN=evil.example.com')
550+
}
551+
]
552+
],
553+
[
554+
[
555+
{ type: oid.localityName, value: UTF8String.encode('Somewhere') },
556+
{ type: oid.commonName, value: UTF8String.encode('evil.example.com') },
557+
]
558+
],
559+
[
560+
[
561+
{
562+
type: oid.localityName,
563+
value: UTF8String.encode('Somewhere + CN=evil.example.com'),
564+
}
565+
]
566+
],
567+
[
568+
[
569+
{ type: oid.localityName, value: UTF8String.encode('L1') },
570+
{ type: oid.localityName, value: UTF8String.encode('L2') },
571+
],
572+
[
573+
{ type: oid.localityName, value: UTF8String.encode('L3') },
574+
]
575+
],
576+
[
577+
[
578+
{ type: oid.localityName, value: UTF8String.encode('L1') },
579+
],
580+
[
581+
{ type: oid.localityName, value: UTF8String.encode('L2') },
582+
],
583+
[
584+
{ type: oid.localityName, value: UTF8String.encode('L3') },
585+
],
586+
],
587+
];
588+
589+
for (let i = 0; i < subjects.length; i++) {
590+
const tbs = {
591+
version: 'v3',
592+
serialNumber: new BN('01', 16),
593+
signature: {
594+
algorithm: oid.sha256WithRSAEncryption,
595+
parameters: null_
596+
},
597+
issuer: {
598+
type: 'rdnSequence',
599+
value: subjects[i]
600+
},
601+
validity: {
602+
notBefore: { type: 'utcTime', value: now },
603+
notAfter: { type: 'utcTime', value: now + days * 86400000 }
604+
},
605+
subject: {
606+
type: 'rdnSequence',
607+
value: subjects[i]
608+
},
609+
subjectPublicKeyInfo: {
610+
algorithm: {
611+
algorithm: oid.rsaEncryption,
612+
parameters: null_
613+
},
614+
subjectPublicKey: {
615+
unused: 0,
616+
data: publicKey
617+
}
618+
}
619+
};
620+
621+
// Self-sign the certificate.
622+
const tbsDer = rfc5280.TBSCertificate.encode(tbs, 'der');
623+
const signature = crypto.createSign(digest).update(tbsDer).sign(privateKey);
624+
625+
// Construct the signed certificate.
626+
const cert = {
627+
tbsCertificate: tbs,
628+
signatureAlgorithm: {
629+
algorithm: oid.sha256WithRSAEncryption,
630+
parameters: null_
631+
},
632+
signature: {
633+
unused: 0,
634+
data: signature
635+
}
636+
};
637+
638+
// Store the signed certificate.
639+
const pem = rfc5280.Certificate.encode(cert, 'pem', {
640+
label: 'CERTIFICATE'
641+
});
642+
writeFileSync(`./subj-${i}-cert.pem`, `${pem}\n`);
643+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIE1zCCAr+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAvMRIwEAYDVQQHDAlTb21l
3+
d2hlcmUxGTAXBgNVBAMMEGV2aWwuZXhhbXBsZS5jb20wHhcNMjExMjIwMTQ1NzM1
4+
WhcNMzExMjE4MTQ1NzM1WjAvMRIwEAYDVQQHDAlTb21ld2hlcmUxGTAXBgNVBAMM
5+
EGV2aWwuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
6+
AQCxEWd00u9E9T/ko6WcCKjhZ7tjnfVylnA7M0EHOwvdivgD46eAb1omsonLagiV
7+
rZrG7EpYuMhtz+g3Yv1d0nvFvv8ge9UIdnN8EDTDzLpJ3KbNqHURraiXuBDqa3rd
8+
Y4JBakCcuYHl1bj1OTew7xl1FWc1je04rBTQGTFIRdmJZYyc9bIw9WkY6msod0w1
9+
PDcLhZS3emh/eYaL4zAQWrVhQfWzf4rZzFaI/a5n0o75aUkTuvxDDQ51V2d6WkSU
10+
3KbOnf2JW+QJXfzsNOeiYA9AnfY59evr4GEeG8VZdGuUG39uDCIWmAUT8elhXXqV
11+
q+NdBqc6VUNLDJbqCmMx/Ecp48EHO6X5uXm0xViZIVPNIzqiiRhVt4nFfwPQZrTg
12+
aq2+tD7/zD1yED4O1FhlDl5twH2N7+oG06HsEluQdLPrj7IedpneGVKMs078Ddov
13+
7j6icYv/RZHVetDlrzDDHjLJWwxyAWzdGdkhtMGPd6B9i4TtF/PU3J3nbpLn5XfE
14+
BFu4jJ+w+5Wvk5a60gF1ERy/OLBM/e8sro2sEBIpp1tN1wJVBZOtTIi4VVDhwDRQ
15+
Uiwb2d1Re7GQ7+mcz5D/01qxW6S+w0IKrpwJUjR3mpa0OU98KfKVJkeyeEBLkEhD
16+
dnGTDqZ9E/ickGosrW2gAAYKgzXk725dpxTdpLEosfDbpwIDAQABMA0GCSqGSIb3
17+
DQEBCwUAA4ICAQAnszSuVqfEmpjf2VMvk9TUuiop0tejHP+hB30IURJqA9K51edx
18+
IRszXXU4Sj8uHT88RpKxgDm/GcfEA0l2rWZ6Mal6pmUyjteJJPMVA6fgeNM8XvtJ
19+
eoxi2wm8FzxXJrPK7fOMG5/fLb7ENUZYFRHVFJ+Gk290DP7x81Gzb5tcsolrVqW+
20+
TZdV2aBZya28NjgXncjinIlD61I6LzoQbDInab5nEPKMRuRTXMLfbAypXrPAbsfz
21+
+Z6ZKhfNEo0/5cI4iG8MQXM1HgbFCkWOTPPeR53lo+1f9dN3IZ+1PYUjkOJzuxUZ
22+
HIA+Dy+S1ocfK582LqohexhjeC5AL74rJJcgns9ORxz2GN1buIRTzi9XL2egp7cd
23+
+XgZ3phpY4mIM0bH+DJ7eIqkM17WkEwJ3vazu7tEmIldc06Pmt2vFEcQB3T0bsw7
24+
lBZdwSEkqTb+IexaQerSyztuxKc2DhOLTqZfVPCd2LWhasNSHzGmanI3vmBy98MN
25+
LZzo7+G1BDMyMsl3DwEiwOGYARXJklU1LxCj6nVCTymNToLXtF2xHcZuK94Pqol9
26+
n8zMCUYNOr7USWA25GwfpN65UHN7YXsOl9XIMWl+iVA5QepAI9sL0n3CyFW0ZXgn
27+
DsZkfikYa+xhQSUANV4zDx1X8FxZmT0Op/+mhkvwL1+YKUHJy3WdXrIFgw==
28+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQHDBpTb21l
3+
d2hlcmUAZXZpbC5leGFtcGxlLmNvbTAeFw0yMTEyMjAxNDU3MzVaFw0zMTEyMTgx
4+
NDU3MzVaMCUxIzAhBgNVBAcMGlNvbWV3aGVyZQBldmlsLmV4YW1wbGUuY29tMIIC
5+
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsRFndNLvRPU/5KOlnAio4We7
6+
Y531cpZwOzNBBzsL3Yr4A+OngG9aJrKJy2oIla2axuxKWLjIbc/oN2L9XdJ7xb7/
7+
IHvVCHZzfBA0w8y6Sdymzah1Ea2ol7gQ6mt63WOCQWpAnLmB5dW49Tk3sO8ZdRVn
8+
NY3tOKwU0BkxSEXZiWWMnPWyMPVpGOprKHdMNTw3C4WUt3pof3mGi+MwEFq1YUH1
9+
s3+K2cxWiP2uZ9KO+WlJE7r8Qw0OdVdnelpElNymzp39iVvkCV387DTnomAPQJ32
10+
OfXr6+BhHhvFWXRrlBt/bgwiFpgFE/HpYV16lavjXQanOlVDSwyW6gpjMfxHKePB
11+
Bzul+bl5tMVYmSFTzSM6ookYVbeJxX8D0Ga04GqtvrQ+/8w9chA+DtRYZQ5ebcB9
12+
je/qBtOh7BJbkHSz64+yHnaZ3hlSjLNO/A3aL+4+onGL/0WR1XrQ5a8wwx4yyVsM
13+
cgFs3RnZIbTBj3egfYuE7Rfz1Nyd526S5+V3xARbuIyfsPuVr5OWutIBdREcvziw
14+
TP3vLK6NrBASKadbTdcCVQWTrUyIuFVQ4cA0UFIsG9ndUXuxkO/pnM+Q/9NasVuk
15+
vsNCCq6cCVI0d5qWtDlPfCnylSZHsnhAS5BIQ3Zxkw6mfRP4nJBqLK1toAAGCoM1
16+
5O9uXacU3aSxKLHw26cCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAmjKOoKxLwPY4
17+
e65pYTUSBctPZ2juW5uNs8UvH5O32OC9RhENJBIIKn3B9Z/wkexR2zcvaQmJObLW
18+
6mkR7O0tNgsXVYJFzLRBfjM/nyP6nafiCUekmoh9Kojq6x5IQQgEsK+Uw123kkoI
19+
w/h3hBYBq8+CFPnYtBLZBVVFMNGaATXrYJPCcjVrtAHYxIWaDN2R+1DWLRIV72sF
20+
hu4xGz0kmUbzforl/FA3gdgM7mwfZMF4+EoQZi5mShdWnyfzAHIbtahnA4lPNtx9
21+
vBqYIZ/a2ITsXmWc2KGs/rRG+SDLzg+H1Xudvu/y2d1ULpZQfT6bg6Ro855FiU9h
22+
TyHHQGGqlC9/DjHy//wERsFEJZh5/j21LGyalEjgfOYtzPkjZlIweYr8LlHTrauo
23+
/gWihriaaWAkD+2fwQ09CUHdvOG6yoT+j/E50FsekfqV3tKMwoZoph6dF1TWQg32
24+
JXV0akpd5ff1cca8sZgJfUksDfSkrwG7fl3tje30vQTlvNrhu2MCKFGQwyXed3qg
25+
86lx+sTZjxMYvqWWysKTx8aIJ95XAK2jJ2OEVI2X6cdgoAp6aMkycbttik4hDoPJ
26+
eAWaZo2UFs2MGoUbX9m4RzPqPuBHNFqoV6yRyS1K/3KWyxVVvamZY0Qgzmoi4coB
27+
hRlTO6GDkF7u1YQ7eZi7pP7U8OcklfE=
28+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)