tls: remove prototype primordials

Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/53699
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Antoine du Hamel 2024-07-07 02:56:04 +02:00 committed by GitHub
parent c86ecfa94a
commit daea065f24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 68 additions and 96 deletions

View File

@ -9,6 +9,7 @@ important than reliability against prototype pollution:
* `node:http` * `node:http`
* `node:http2` * `node:http2`
* `node:tls`
Usage of primordials should be preferred for new code in other areas, but Usage of primordials should be preferred for new code in other areas, but
replacing current code with primordials should be replacing current code with primordials should be

View File

@ -22,9 +22,7 @@
'use strict'; 'use strict';
const { const {
ArrayPrototypePush,
JSONParse, JSONParse,
RegExpPrototypeSymbolReplace,
} = primordials; } = primordials;
const tls = require('tls'); const tls = require('tls');
@ -133,21 +131,21 @@ function translatePeerCertificate(c) {
c.infoAccess = { __proto__: null }; c.infoAccess = { __proto__: null };
// XXX: More key validation? // XXX: More key validation?
RegExpPrototypeSymbolReplace(/([^\n:]*):([^\n]*)(?:\n|$)/g, info, info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g,
(all, key, val) => { (all, key, val) => {
if (val.charCodeAt(0) === 0x22) { if (val.charCodeAt(0) === 0x22) {
// The translatePeerCertificate function is only // The translatePeerCertificate function is only
// used on internally created legacy certificate // used on internally created legacy certificate
// objects, and any value that contains a quote // objects, and any value that contains a quote
// will always be a valid JSON string literal, // will always be a valid JSON string literal,
// so this should never throw. // so this should never throw.
val = JSONParse(val); val = JSONParse(val);
} }
if (key in c.infoAccess) if (key in c.infoAccess)
ArrayPrototypePush(c.infoAccess[key], val); c.infoAccess[key].push(val);
else else
c.infoAccess[key] = [val]; c.infoAccess[key] = [val];
}); });
} }
return c; return c;
} }

View File

@ -22,19 +22,11 @@
'use strict'; 'use strict';
const { const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypePush,
FunctionPrototype,
ObjectAssign, ObjectAssign,
ObjectDefineProperty, ObjectDefineProperty,
ObjectSetPrototypeOf, ObjectSetPrototypeOf,
ReflectApply, ReflectApply,
RegExp, RegExp,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
StringPrototypeReplaceAll,
StringPrototypeSlice,
Symbol, Symbol,
SymbolFor, SymbolFor,
} = primordials; } = primordials;
@ -119,7 +111,7 @@ const kPskIdentityHint = Symbol('pskidentityhint');
const kPendingSession = Symbol('pendingSession'); const kPendingSession = Symbol('pendingSession');
const kIsVerified = Symbol('verified'); const kIsVerified = Symbol('verified');
const noop = FunctionPrototype; const noop = () => {};
let ipServernameWarned = false; let ipServernameWarned = false;
let tlsTracingWarned = false; let tlsTracingWarned = false;
@ -475,8 +467,7 @@ function onerror(err) {
owner.destroy(err); owner.destroy(err);
} else if (owner._tlsOptions?.isServer && } else if (owner._tlsOptions?.isServer &&
owner._rejectUnauthorized && owner._rejectUnauthorized &&
RegExpPrototypeExec(/peer did not return a certificate/, /peer did not return a certificate/.test(err.message)) {
err.message) !== null) {
// Ignore server's authorization errors // Ignore server's authorization errors
owner.destroy(); owner.destroy();
} else { } else {
@ -1162,7 +1153,7 @@ function makeSocketMethodProxy(name) {
}; };
} }
ArrayPrototypeForEach([ [
'getCipher', 'getCipher',
'getSharedSigalgs', 'getSharedSigalgs',
'getEphemeralKeyInfo', 'getEphemeralKeyInfo',
@ -1173,7 +1164,7 @@ ArrayPrototypeForEach([
'getTLSTicket', 'getTLSTicket',
'isSessionReused', 'isSessionReused',
'enableTrace', 'enableTrace',
], (method) => { ].forEach((method) => {
TLSSocket.prototype[method] = makeSocketMethodProxy(method); TLSSocket.prototype[method] = makeSocketMethodProxy(method);
}); });
@ -1470,10 +1461,10 @@ Server.prototype.setSecureContext = function(options) {
if (options.sessionIdContext) { if (options.sessionIdContext) {
this.sessionIdContext = options.sessionIdContext; this.sessionIdContext = options.sessionIdContext;
} else { } else {
this.sessionIdContext = StringPrototypeSlice( this.sessionIdContext = crypto.createHash('sha1')
crypto.createHash('sha1') .update(process.argv.join(' '))
.update(ArrayPrototypeJoin(process.argv, ' ')) .digest('hex')
.digest('hex'), 0, 32); .slice(0, 32);
} }
if (options.sessionTimeout) if (options.sessionTimeout)
@ -1568,10 +1559,10 @@ Server.prototype.setOptions = deprecate(function(options) {
if (options.sessionIdContext) { if (options.sessionIdContext) {
this.sessionIdContext = options.sessionIdContext; this.sessionIdContext = options.sessionIdContext;
} else { } else {
this.sessionIdContext = StringPrototypeSlice( this.sessionIdContext = crypto.createHash('sha1')
crypto.createHash('sha1') .update(process.argv.join(' '))
.update(ArrayPrototypeJoin(process.argv, ' ')) .digest('hex')
.digest('hex'), 0, 32); .slice(0, 32);
} }
if (options.pskCallback) this[kPskCallback] = options.pskCallback; if (options.pskCallback) this[kPskCallback] = options.pskCallback;
if (options.pskIdentityHint) this[kPskIdentityHint] = options.pskIdentityHint; if (options.pskIdentityHint) this[kPskIdentityHint] = options.pskIdentityHint;
@ -1588,14 +1579,15 @@ Server.prototype.addContext = function(servername, context) {
throw new ERR_TLS_REQUIRED_SERVER_NAME(); throw new ERR_TLS_REQUIRED_SERVER_NAME();
} }
const re = new RegExp('^' + StringPrototypeReplaceAll( const re = new RegExp(`^${
RegExpPrototypeSymbolReplace(/([.^$+?\-\\[\]{}])/g, servername, '\\$1'), servername
'*', '[^.]*', .replace(/([.^$+?\-\\[\]{}])/g, '\\$1')
) + '$'); .replaceAll('*', '[^.]*')
}$`);
const secureContext = const secureContext =
context instanceof common.SecureContext ? context : tls.createSecureContext(context); context instanceof common.SecureContext ? context : tls.createSecureContext(context);
ArrayPrototypePush(this._contexts, [re, secureContext.context]); this._contexts.push([re, secureContext.context]);
}; };
Server.prototype[EE.captureRejectionSymbol] = function( Server.prototype[EE.captureRejectionSymbol] = function(
@ -1616,7 +1608,7 @@ function SNICallback(servername, callback) {
for (let i = contexts.length - 1; i >= 0; --i) { for (let i = contexts.length - 1; i >= 0; --i) {
const elem = contexts[i]; const elem = contexts[i];
if (RegExpPrototypeExec(elem[0], servername) !== null) { if (elem[0].test(servername)) {
callback(null, elem[1]); callback(null, elem[1]);
return; return;
} }

View File

@ -492,16 +492,18 @@ export default [
{ {
files: [ files: [
'lib/_http_*.js', 'lib/_http_*.js',
'lib/_tls_*.js',
'lib/http.js', 'lib/http.js',
'lib/http2.js', 'lib/http2.js',
'lib/internal/http.js', 'lib/internal/http.js',
'lib/internal/http2/*.js', 'lib/internal/http2/*.js',
'lib/tls.js',
], ],
rules: { rules: {
'no-restricted-syntax': [ 'no-restricted-syntax': [
...noRestrictedSyntax, ...noRestrictedSyntax,
{ {
selector: 'VariableDeclarator:has(.init[name="primordials"]) Identifier[name=/Prototype/]:not([name=/^(Object|Reflect)(Get|Set)PrototypeOf$/])', selector: 'VariableDeclarator:has(.init[name="primordials"]) Identifier[name=/Prototype[A-Z]/]:not([name=/^(Object|Reflect)(Get|Set)PrototypeOf$/])',
message: 'We do not use prototype primordials in this file', message: 'We do not use prototype primordials in this file',
}, },
], ],

View File

@ -24,26 +24,10 @@
const { const {
Array, Array,
ArrayIsArray, ArrayIsArray,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypeReduce,
ArrayPrototypeSome,
JSONParse, JSONParse,
ObjectDefineProperty, ObjectDefineProperty,
ObjectFreeze, ObjectFreeze,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
StringFromCharCode, StringFromCharCode,
StringPrototypeCharCodeAt,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
StringPrototypeSubstring,
} = primordials; } = primordials;
const { const {
@ -122,7 +106,7 @@ ObjectDefineProperty(exports, 'rootCertificates', {
// ("\x06spdy/2\x08http/1.1\x08http/1.0") // ("\x06spdy/2\x08http/1.1\x08http/1.0")
function convertProtocols(protocols) { function convertProtocols(protocols) {
const lens = new Array(protocols.length); const lens = new Array(protocols.length);
const buff = Buffer.allocUnsafe(ArrayPrototypeReduce(protocols, (p, c, i) => { const buff = Buffer.allocUnsafe(protocols.reduce((p, c, i) => {
const len = Buffer.byteLength(c); const len = Buffer.byteLength(c);
if (len > 255) { if (len > 255) {
throw new ERR_OUT_OF_RANGE('The byte length of the protocol at index ' + throw new ERR_OUT_OF_RANGE('The byte length of the protocol at index ' +
@ -158,20 +142,17 @@ exports.convertALPNProtocols = function convertALPNProtocols(protocols, out) {
}; };
function unfqdn(host) { function unfqdn(host) {
return RegExpPrototypeSymbolReplace(/[.]$/, host, ''); return host.replace(/[.]$/, '');
} }
// String#toLowerCase() is locale-sensitive so we use // String#toLowerCase() is locale-sensitive so we use
// a conservative version that only lowercases A-Z. // a conservative version that only lowercases A-Z.
function toLowerCase(c) { function toLowerCase(c) {
return StringFromCharCode(32 + StringPrototypeCharCodeAt(c, 0)); return StringFromCharCode(32 + c.charCodeAt(0));
} }
function splitHost(host) { function splitHost(host) {
return StringPrototypeSplit( return unfqdn(host).replace(/[A-Z]/g, toLowerCase).split('.');
RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host), toLowerCase),
'.',
);
} }
function check(hostParts, pattern, wildcards) { function check(hostParts, pattern, wildcards) {
@ -185,15 +166,15 @@ function check(hostParts, pattern, wildcards) {
return false; return false;
// Pattern has empty components, e.g. "bad..example.com". // Pattern has empty components, e.g. "bad..example.com".
if (ArrayPrototypeIncludes(patternParts, '')) if (patternParts.includes(''))
return false; return false;
// RFC 6125 allows IDNA U-labels (Unicode) in names but we have no // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
// good way to detect their encoding or normalize them so we simply // good way to detect their encoding or normalize them so we simply
// reject them. Control characters and blanks are rejected as well // reject them. Control characters and blanks are rejected as well
// because nothing good can come from accepting them. // because nothing good can come from accepting them.
const isBad = (s) => RegExpPrototypeExec(/[^\u0021-\u007F]/u, s) !== null; const isBad = (s) => /[^\u0021-\u007F]/u.test(s);
if (ArrayPrototypeSome(patternParts, isBad)) if (patternParts.some(isBad))
return false; return false;
// Check host parts from right to left first. // Check host parts from right to left first.
@ -204,13 +185,13 @@ function check(hostParts, pattern, wildcards) {
const hostSubdomain = hostParts[0]; const hostSubdomain = hostParts[0];
const patternSubdomain = patternParts[0]; const patternSubdomain = patternParts[0];
const patternSubdomainParts = StringPrototypeSplit(patternSubdomain, '*'); const patternSubdomainParts = patternSubdomain.split('*');
// Short-circuit when the subdomain does not contain a wildcard. // Short-circuit when the subdomain does not contain a wildcard.
// RFC 6125 does not allow wildcard substitution for components // RFC 6125 does not allow wildcard substitution for components
// containing IDNA A-labels (Punycode) so match those verbatim. // containing IDNA A-labels (Punycode) so match those verbatim.
if (patternSubdomainParts.length === 1 || if (patternSubdomainParts.length === 1 ||
StringPrototypeIncludes(patternSubdomain, 'xn--')) patternSubdomain.includes('xn--'))
return hostSubdomain === patternSubdomain; return hostSubdomain === patternSubdomain;
if (!wildcards) if (!wildcards)
@ -229,10 +210,10 @@ function check(hostParts, pattern, wildcards) {
if (prefix.length + suffix.length > hostSubdomain.length) if (prefix.length + suffix.length > hostSubdomain.length)
return false; return false;
if (!StringPrototypeStartsWith(hostSubdomain, prefix)) if (!hostSubdomain.startsWith(prefix))
return false; return false;
if (!StringPrototypeEndsWith(hostSubdomain, suffix)) if (!hostSubdomain.endsWith(suffix))
return false; return false;
return true; return true;
@ -250,13 +231,12 @@ function splitEscapedAltNames(altNames) {
let currentToken = ''; let currentToken = '';
let offset = 0; let offset = 0;
while (offset !== altNames.length) { while (offset !== altNames.length) {
const nextSep = StringPrototypeIndexOf(altNames, ', ', offset); const nextSep = altNames.indexOf(',', offset);
const nextQuote = StringPrototypeIndexOf(altNames, '"', offset); const nextQuote = altNames.indexOf('"', offset);
if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) {
// There is a quote character and there is no separator before the quote. // There is a quote character and there is no separator before the quote.
currentToken += StringPrototypeSubstring(altNames, offset, nextQuote); currentToken += altNames.substring(offset, nextQuote);
const match = RegExpPrototypeExec( const match = jsonStringPattern.exec(altNames.substring(nextQuote));
jsonStringPattern, StringPrototypeSubstring(altNames, nextQuote));
if (!match) { if (!match) {
throw new ERR_TLS_CERT_ALTNAME_FORMAT(); throw new ERR_TLS_CERT_ALTNAME_FORMAT();
} }
@ -264,16 +244,16 @@ function splitEscapedAltNames(altNames) {
offset = nextQuote + match[0].length; offset = nextQuote + match[0].length;
} else if (nextSep !== -1) { } else if (nextSep !== -1) {
// There is a separator and no quote before it. // There is a separator and no quote before it.
currentToken += StringPrototypeSubstring(altNames, offset, nextSep); currentToken += altNames.substring(offset, nextSep);
ArrayPrototypePush(result, currentToken); result.push(currentToken);
currentToken = ''; currentToken = '';
offset = nextSep + 2; offset = nextSep + 2;
} else { } else {
currentToken += StringPrototypeSubstring(altNames, offset); currentToken += altNames.substring(offset);
offset = altNames.length; offset = altNames.length;
} }
} }
ArrayPrototypePush(result, currentToken); result.push(currentToken);
return result; return result;
} }
@ -286,14 +266,14 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
hostname = '' + hostname; hostname = '' + hostname;
if (altNames) { if (altNames) {
const splitAltNames = StringPrototypeIncludes(altNames, '"') ? const splitAltNames = altNames.includes('"') ?
splitEscapedAltNames(altNames) : splitEscapedAltNames(altNames) :
StringPrototypeSplit(altNames, ', '); altNames.split(', ');
ArrayPrototypeForEach(splitAltNames, (name) => { splitAltNames.forEach((name) => {
if (StringPrototypeStartsWith(name, 'DNS:')) { if (name.startsWith('DNS:')) {
ArrayPrototypePush(dnsNames, StringPrototypeSlice(name, 4)); dnsNames.push(name.slice(4));
} else if (StringPrototypeStartsWith(name, 'IP Address:')) { } else if (name.startsWith('IP Address:')) {
ArrayPrototypePush(ips, canonicalizeIP(StringPrototypeSlice(name, 11))); ips.push(canonicalizeIP(name.slice(11)));
} }
}); });
} }
@ -304,16 +284,15 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
hostname = unfqdn(hostname); // Remove trailing dot for error messages. hostname = unfqdn(hostname); // Remove trailing dot for error messages.
if (net.isIP(hostname)) { if (net.isIP(hostname)) {
valid = ArrayPrototypeIncludes(ips, canonicalizeIP(hostname)); valid = ips.includes(canonicalizeIP(hostname));
if (!valid) if (!valid)
reason = `IP: ${hostname} is not in the cert's list: ` + reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(', ');
ArrayPrototypeJoin(ips, ', ');
} else if (dnsNames.length > 0 || subject?.CN) { } else if (dnsNames.length > 0 || subject?.CN) {
const hostParts = splitHost(hostname); const hostParts = splitHost(hostname);
const wildcard = (pattern) => check(hostParts, pattern, true); const wildcard = (pattern) => check(hostParts, pattern, true);
if (dnsNames.length > 0) { if (dnsNames.length > 0) {
valid = ArrayPrototypeSome(dnsNames, wildcard); valid = dnsNames.some(wildcard);
if (!valid) if (!valid)
reason = reason =
`Host: ${hostname}. is not in the cert's altnames: ${altNames}`; `Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
@ -322,7 +301,7 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
const cn = subject.CN; const cn = subject.CN;
if (ArrayIsArray(cn)) if (ArrayIsArray(cn))
valid = ArrayPrototypeSome(cn, wildcard); valid = cn.some(wildcard);
else if (cn) else if (cn)
valid = wildcard(cn); valid = wildcard(cn);