Skip to content

Commit 075cfbf

Browse files
guybedfordtargos
authored andcommitted
module: resolver & spec hardening /w refactoring
PR-URL: #40510 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 8a00dc5 commit 075cfbf

File tree

3 files changed

+88
-89
lines changed

3 files changed

+88
-89
lines changed

doc/api/esm.md

+52-44
Original file line numberDiff line numberDiff line change
@@ -1076,59 +1076,63 @@ The resolver can throw the following errors:
10761076
> 1. Note: _specifier_ is now a bare specifier.
10771077
> 2. Set _resolved_ the result of
10781078
> **PACKAGE\_RESOLVE**(_specifier_, _parentURL_).
1079-
> 6. If _resolved_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_
1080-
> and _"%5C"_ respectively), then
1081-
> 1. Throw an _Invalid Module Specifier_ error.
1082-
> 7. If the file at _resolved_ is a directory, then
1083-
> 1. Throw an _Unsupported Directory Import_ error.
1084-
> 8. If the file at _resolved_ does not exist, then
1085-
> 1. Throw a _Module Not Found_ error.
1086-
> 9. Set _resolved_ to the real path of _resolved_.
1087-
> 10. Let _format_ be the result of **ESM\_FORMAT**(_resolved_).
1088-
> 11. Load _resolved_ as module format, _format_.
1089-
> 12. Return _resolved_.
1079+
> 6. Let _format_ be **undefined**.
1080+
> 7. If _resolved_ is a _"file:"_ URL, then
1081+
> 1. If _resolved_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2F"_
1082+
> and _"%5C"_ respectively), then
1083+
> 1. Throw an _Invalid Module Specifier_ error.
1084+
> 2. If the file at _resolved_ is a directory, then
1085+
> 1. Throw an _Unsupported Directory Import_ error.
1086+
> 3. If the file at _resolved_ does not exist, then
1087+
> 1. Throw a _Module Not Found_ error.
1088+
> 4. Set _resolved_ to the real path of _resolved_, maintaining the
1089+
> same URL querystring and fragment components.
1090+
> 5. Set _format_ to the result of **ESM\_FILE\_FORMAT**(_resolved_).
1091+
> 8. Otherwise,
1092+
> 1. Set _format_ the module format of the content type associated with the
1093+
> URL _resolved_.
1094+
> 9. Load _resolved_ as module format, _format_.
10901095

10911096
**PACKAGE\_RESOLVE**(_packageSpecifier_, _parentURL_)
10921097

10931098
> 1. Let _packageName_ be **undefined**.
10941099
> 2. If _packageSpecifier_ is an empty string, then
10951100
> 1. Throw an _Invalid Module Specifier_ error.
1096-
> 3. If _packageSpecifier_ does not start with _"@"_, then
1101+
> 3. If _packageSpecifier_ is a Node.js builtin module name, then
1102+
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_.
1103+
> 4. If _packageSpecifier_ does not start with _"@"_, then
10971104
> 1. Set _packageName_ to the substring of _packageSpecifier_ until the first
10981105
> _"/"_ separator or the end of the string.
1099-
> 4. Otherwise,
1106+
> 5. Otherwise,
11001107
> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then
11011108
> 1. Throw an _Invalid Module Specifier_ error.
11021109
> 2. Set _packageName_ to the substring of _packageSpecifier_
11031110
> until the second _"/"_ separator or the end of the string.
1104-
> 5. If _packageName_ starts with _"."_ or contains _"\\"_ or _"%"_, then
1111+
> 6. If _packageName_ starts with _"."_ or contains _"\\"_ or _"%"_, then
11051112
> 1. Throw an _Invalid Module Specifier_ error.
1106-
> 6. Let _packageSubpath_ be _"."_ concatenated with the substring of
1113+
> 7. Let _packageSubpath_ be _"."_ concatenated with the substring of
11071114
> _packageSpecifier_ from the position at the length of _packageName_.
1108-
> 7. If _packageSubpath_ ends in _"/"_, then
1115+
> 8. If _packageSubpath_ ends in _"/"_, then
11091116
> 1. Throw an _Invalid Module Specifier_ error.
1110-
> 8. Let _selfUrl_ be the result of
1117+
> 9. Let _selfUrl_ be the result of
11111118
> **PACKAGE\_SELF\_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_).
1112-
> 9. If _selfUrl_ is not **undefined**, return _selfUrl_.
1113-
> 10. If _packageSubpath_ is _"."_ and _packageName_ is a Node.js builtin
1114-
> module, then
1115-
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_.
1119+
> 10. If _selfUrl_ is not **undefined**, return _selfUrl_.
11161120
> 11. While _parentURL_ is not the file system root,
1117-
> 1. Let _packageURL_ be the URL resolution of _"node\_modules/"_
1118-
> concatenated with _packageSpecifier_, relative to _parentURL_.
1119-
> 2. Set _parentURL_ to the parent folder URL of _parentURL_.
1120-
> 3. If the folder at _packageURL_ does not exist, then
1121-
> 1. Continue the next loop iteration.
1122-
> 4. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1123-
> 5. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
1124-
> **undefined**, then
1125-
> 1. Return the result of **PACKAGE\_EXPORTS\_RESOLVE**(_packageURL_,
1126-
> _packageSubpath_, _pjson.exports_, _defaultConditions_).
1127-
> 6. Otherwise, if _packageSubpath_ is equal to _"."_, then
1128-
> 1. If _pjson.main_ is a string, then
1129-
> 1. Return the URL resolution of _main_ in _packageURL_.
1130-
> 7. Otherwise,
1131-
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
1121+
> 1. Let _packageURL_ be the URL resolution of _"node_modules/"_
1122+
> concatenated with _packageSpecifier_, relative to _parentURL_.
1123+
> 2. Set _parentURL_ to the parent folder URL of _parentURL_.
1124+
> 3. If the folder at _packageURL_ does not exist, then
1125+
> 1. Continue the next loop iteration.
1126+
> 4. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1127+
> 5. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
1128+
> **undefined**, then
1129+
> 1. Return the result of **PACKAGE\_EXPORTS\_RESOLVE**(_packageURL_,
1130+
> _packageSubpath_, _pjson.exports_, _defaultConditions_).
1131+
> 6. Otherwise, if _packageSubpath_ is equal to _"."_, then
1132+
> 1. If _pjson.main_ is a string, then
1133+
> 1. Return the URL resolution of _main_ in _packageURL_.
1134+
> 7. Otherwise,
1135+
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
11321136
> 12. Throw a _Module Not Found_ error.
11331137

11341138
**PACKAGE\_SELF\_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_)
@@ -1238,18 +1242,20 @@ _internal_, _conditions_)
12381242
> _"/"_ and is not a valid URL, then
12391243
> 1. If _pattern_ is **true**, then
12401244
> 1. Return **PACKAGE\_RESOLVE**(_target_ with every instance of
1241-
> _"\*"_ replaced by _subpath_, _packageURL_ + _"/"_)\_.
1245+
> _"\*"_ replaced by _subpath_, _packageURL_ + _"/"_).
12421246
> 2. Return **PACKAGE\_RESOLVE**(_target_ + _subpath_,
1243-
> _packageURL_ + _"/"_)\_.
1247+
> _packageURL_ + _"/"_)_.
12441248
> 2. Otherwise, throw an _Invalid Package Target_ error.
12451249
> 3. If _target_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
1246-
> _"node\_modules"_ segments after the first segment, throw an
1247-
> _Invalid Package Target_ error.
1250+
> _"node\_modules"_ segments after the first segment, case insensitive and
1251+
> including percent encoded variants, throw an _Invalid Package Target_
1252+
> error.
12481253
> 4. Let _resolvedTarget_ be the URL resolution of the concatenation of
12491254
> _packageURL_ and _target_.
12501255
> 5. Assert: _resolvedTarget_ is contained in _packageURL_.
12511256
> 6. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
1252-
> _"node\_modules"_ segments, throw an _Invalid Module Specifier_ error.
1257+
> _"node\_modules"_ segments, case insensitive and including percent
1258+
> encoded variants, throw an _Invalid Module Specifier_ error.
12531259
> 7. If _pattern_ is **true**, then
12541260
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
12551261
> _"\*"_ replaced with _subpath_.
@@ -1282,19 +1288,21 @@ _internal_, _conditions_)
12821288
> 4. Otherwise, if _target_ is _null_, return **null**.
12831289
> 5. Otherwise throw an _Invalid Package Target_ error.
12841290

1285-
**ESM\_FORMAT**(_url_)
1291+
**ESM\_FILE\_FORMAT**(_url_)
12861292

12871293
> 1. Assert: _url_ corresponds to an existing file.
12881294
> 2. Let _pjson_ be the result of **READ\_PACKAGE\_SCOPE**(_url_).
12891295
> 3. If _url_ ends in _".mjs"_, then
12901296
> 1. Return _"module"_.
12911297
> 4. If _url_ ends in _".cjs"_, then
12921298
> 1. Return _"commonjs"_.
1293-
> 5. If _pjson?.type_ exists and is _"module"_, then
1299+
> 5. If _url_ ends in _".json"_, then
1300+
> 1. Return _"json"_.
1301+
> 6. If _pjson?.type_ exists and is _"module"_, then
12941302
> 1. If _url_ ends in _".js"_, then
12951303
> 1. Return _"module"_.
12961304
> 2. Throw an _Unsupported File Extension_ error.
1297-
> 6. Otherwise,
1305+
> 7. Otherwise,
12981306
> 1. Throw an _Unsupported File Extension_ error.
12991307

13001308
**READ\_PACKAGE\_SCOPE**(_url_)

lib/internal/modules/esm/resolve.js

+32-45
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
ObjectGetOwnPropertyNames,
1111
ObjectPrototypeHasOwnProperty,
1212
RegExp,
13+
RegExpPrototypeExec,
1314
RegExpPrototypeSymbolReplace,
1415
RegExpPrototypeTest,
1516
SafeMap,
@@ -360,9 +361,10 @@ const encodedSepRegEx = /%2F|%5C/i;
360361
/**
361362
* @param {URL} resolved
362363
* @param {string | URL | undefined} base
364+
* @param {boolean} preserveSymlinks
363365
* @returns {URL | undefined}
364366
*/
365-
function finalizeResolution(resolved, base) {
367+
function finalizeResolution(resolved, base, preserveSymlinks) {
366368
if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname))
367369
throw new ERR_INVALID_MODULE_SPECIFIER(
368370
resolved.pathname, 'must not include encoded "/" or "\\" characters',
@@ -393,6 +395,17 @@ function finalizeResolution(resolved, base) {
393395
path || resolved.pathname, base && fileURLToPath(base), 'module');
394396
}
395397

398+
if (!preserveSymlinks) {
399+
const real = realpathSync(path, {
400+
[internalFS.realpathCacheKey]: realpathCache
401+
});
402+
const { search, hash } = resolved;
403+
resolved =
404+
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));
405+
resolved.search = search;
406+
resolved.hash = hash;
407+
}
408+
396409
return resolved;
397410
}
398411

@@ -444,7 +457,8 @@ function throwInvalidPackageTarget(
444457
internal, base && fileURLToPath(base));
445458
}
446459

447-
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
460+
const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
461+
const invalidPackageNameRegEx = /^\.|%|\\/;
448462
const patternRegEx = /\*/g;
449463

450464
function resolvePackageTargetString(
@@ -777,13 +791,9 @@ function parsePackageName(specifier, base) {
777791
specifier : StringPrototypeSlice(specifier, 0, separatorIndex);
778792

779793
// Package name cannot have leading . and cannot have percent-encoding or
780-
// separators.
781-
for (let i = 0; i < packageName.length; i++) {
782-
if (packageName[i] === '%' || packageName[i] === '\\') {
783-
validPackageName = false;
784-
break;
785-
}
786-
}
794+
// \\ separators.
795+
if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null)
796+
validPackageName = false;
787797

788798
if (!validPackageName) {
789799
throw new ERR_INVALID_MODULE_SPECIFIER(
@@ -803,6 +813,9 @@ function parsePackageName(specifier, base) {
803813
* @returns {URL}
804814
*/
805815
function packageResolve(specifier, base, conditions) {
816+
if (NativeModule.canBeRequiredByUsers(specifier))
817+
return new URL('node:' + specifier);
818+
806819
const { packageName, packageSubpath, isScoped } =
807820
parsePackageName(specifier, base);
808821

@@ -879,9 +892,10 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
879892
* @param {string} specifier
880893
* @param {string | URL | undefined} base
881894
* @param {Set<string>} conditions
895+
* @param {boolean} preserveSymlinks
882896
* @returns {URL}
883897
*/
884-
function moduleResolve(specifier, base, conditions) {
898+
function moduleResolve(specifier, base, conditions, preserveSymlinks) {
885899
// Order swapped from spec for minor perf gain.
886900
// Ok since relative URLs cannot parse as URLs.
887901
let resolved;
@@ -896,7 +910,9 @@ function moduleResolve(specifier, base, conditions) {
896910
resolved = packageResolve(specifier, base, conditions);
897911
}
898912
}
899-
return finalizeResolution(resolved, base);
913+
if (resolved.protocol !== 'file:')
914+
return resolved;
915+
return finalizeResolution(resolved, base, preserveSymlinks);
900916
}
901917

902918
/**
@@ -968,28 +984,6 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
968984
}
969985
}
970986
}
971-
let parsed;
972-
try {
973-
parsed = new URL(specifier);
974-
if (parsed.protocol === 'data:') {
975-
return {
976-
url: specifier
977-
};
978-
}
979-
} catch {}
980-
if (parsed && parsed.protocol === 'node:')
981-
return { url: specifier };
982-
if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
983-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed);
984-
if (NativeModule.canBeRequiredByUsers(specifier)) {
985-
return {
986-
url: 'node:' + specifier
987-
};
988-
}
989-
if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) {
990-
// This is gonna blow up, we want the error
991-
new URL(specifier, parentURL);
992-
}
993987

994988
const isMain = parentURL === undefined;
995989
if (isMain) {
@@ -1008,7 +1002,8 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10081002
conditions = getConditionsSet(conditions);
10091003
let url;
10101004
try {
1011-
url = moduleResolve(specifier, parentURL, conditions);
1005+
url = moduleResolve(specifier, parentURL, conditions,
1006+
isMain ? preserveSymlinksMain : preserveSymlinks);
10121007
} catch (error) {
10131008
// Try to give the user a hint of what would have been the
10141009
// resolved CommonJS module
@@ -1032,17 +1027,9 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10321027
throw error;
10331028
}
10341029

1035-
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
1036-
const urlPath = fileURLToPath(url);
1037-
const real = realpathSync(urlPath, {
1038-
[internalFS.realpathCacheKey]: realpathCache
1039-
});
1040-
const old = url;
1041-
url = pathToFileURL(
1042-
real + (StringPrototypeEndsWith(urlPath, sep) ? '/' : ''));
1043-
url.search = old.search;
1044-
url.hash = old.hash;
1045-
}
1030+
if (url.protocol !== 'file:' && url.protocol !== 'data:' &&
1031+
url.protocol !== 'node:')
1032+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
10461033

10471034
return { url: `${url}` };
10481035
}

test/es-module/test-esm-exports.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
104104
// Even though 'pkgexports/sub/asdf.js' works, alternate "path-like"
105105
// variants do not to prevent confusion and accidental loopholes.
106106
['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'],
107+
// Cannot reach into node_modules, even percent encoded
108+
['pkgexports/sub/no%64e_modules', './sub/no%64e_modules'],
109+
// Cannot backtrack below exposed path, even with percent encoded "."
110+
['pkgexports/sub/%2e./asdf', './asdf'],
107111
]);
108112

109113
for (const [specifier, subpath] of undefinedExports) {

0 commit comments

Comments
 (0)