Skip to content

Commit e7391ea

Browse files
guybedforddanielleadams
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 ce2dc48 commit e7391ea

File tree

3 files changed

+91
-94
lines changed

3 files changed

+91
-94
lines changed

doc/api/esm.md

+55-49
Original file line numberDiff line numberDiff line change
@@ -1068,62 +1068,64 @@ The resolver can throw the following errors:
10681068
> 1. Note: _specifier_ is now a bare specifier.
10691069
> 2. Set _resolved_ the result of
10701070
> **PACKAGE\_RESOLVE**(_specifier_, _parentURL_).
1071-
> 6. If _resolved_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_
1072-
> and _"%5C"_ respectively), then
1073-
> 1. Throw an _Invalid Module Specifier_ error.
1074-
> 7. If the file at _resolved_ is a directory, then
1075-
> 1. Throw an _Unsupported Directory Import_ error.
1076-
> 8. If the file at _resolved_ does not exist, then
1077-
> 1. Throw a _Module Not Found_ error.
1078-
> 9. Set _resolved_ to the real path of _resolved_.
1079-
> 10. Let _format_ be the result of **ESM\_FORMAT**(_resolved_).
1080-
> 11. Load _resolved_ as module format, _format_.
1081-
> 12. Return _resolved_.
1071+
> 6. Let _format_ be **undefined**.
1072+
> 7. If _resolved_ is a _"file:"_ URL, then
1073+
> 1. If _resolved_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2F"_
1074+
> and _"%5C"_ respectively), then
1075+
> 1. Throw an _Invalid Module Specifier_ error.
1076+
> 2. If the file at _resolved_ is a directory, then
1077+
> 1. Throw an _Unsupported Directory Import_ error.
1078+
> 3. If the file at _resolved_ does not exist, then
1079+
> 1. Throw a _Module Not Found_ error.
1080+
> 4. Set _resolved_ to the real path of _resolved_, maintaining the
1081+
> same URL querystring and fragment components.
1082+
> 5. Set _format_ to the result of **ESM\_FILE\_FORMAT**(_resolved_).
1083+
> 8. Otherwise,
1084+
> 1. Set _format_ the module format of the content type associated with the
1085+
> URL _resolved_.
1086+
> 9. Load _resolved_ as module format, _format_.
10821087

10831088
**PACKAGE\_RESOLVE**(_packageSpecifier_, _parentURL_)
10841089

10851090
> 1. Let _packageName_ be **undefined**.
10861091
> 2. If _packageSpecifier_ is an empty string, then
10871092
> 1. Throw an _Invalid Module Specifier_ error.
1088-
> 3. If _packageSpecifier_ does not start with _"@"_, then
1093+
> 3. If _packageSpecifier_ is a Node.js builtin module name, then
1094+
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_.
1095+
> 4. If _packageSpecifier_ does not start with _"@"_, then
10891096
> 1. Set _packageName_ to the substring of _packageSpecifier_ until the first
10901097
> _"/"_ separator or the end of the string.
1091-
> 4. Otherwise,
1098+
> 5. Otherwise,
10921099
> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then
10931100
> 1. Throw an _Invalid Module Specifier_ error.
10941101
> 2. Set _packageName_ to the substring of _packageSpecifier_
10951102
> until the second _"/"_ separator or the end of the string.
1096-
> 5. If _packageName_ starts with _"."_ or contains _"\\"_ or _"%"_, then
1103+
> 6. If _packageName_ starts with _"."_ or contains _"\\"_ or _"%"_, then
10971104
> 1. Throw an _Invalid Module Specifier_ error.
1098-
> 6. Let _packageSubpath_ be _"."_ concatenated with the substring of
1105+
> 7. Let _packageSubpath_ be _"."_ concatenated with the substring of
10991106
> _packageSpecifier_ from the position at the length of _packageName_.
1100-
> 7. Let _selfUrl_ be the result of
1107+
> 8. If _packageSubpath_ ends in _"/"_, then
1108+
> 1. Throw an _Invalid Module Specifier_ error.
1109+
> 9. Let _selfUrl_ be the result of
11011110
> **PACKAGE\_SELF\_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_).
1102-
> 8. If _selfUrl_ is not **undefined**, return _selfUrl_.
1103-
> 9. If _packageSubpath_ is _"."_ and _packageName_ is a Node.js builtin
1104-
> module, then
1105-
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_.
1106-
> 10. While _parentURL_ is not the file system root,
1107-
> 1. Let _packageURL_ be the URL resolution of _"node\_modules/"_
1108-
> concatenated with _packageSpecifier_, relative to _parentURL_.
1109-
> 2. Set _parentURL_ to the parent folder URL of _parentURL_.
1110-
> 3. If the folder at _packageURL_ does not exist, then
1111-
> 1. Set _parentURL_ to the parent URL path of _parentURL_.
1112-
> 2. Continue the next loop iteration.
1113-
> 4. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1114-
> 5. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
1115-
> **undefined**, then
1116-
> 1. Let _exports_ be _pjson.exports_.
1117-
> 2. Return the _resolved_ destructured value of the result of
1118-
> **PACKAGE\_EXPORTS\_RESOLVE**(_packageURL_, _packageSubpath_,
1119-
> _pjson.exports_, _defaultConditions_).
1120-
> 6. Otherwise, if _packageSubpath_ is equal to _"."_, then
1121-
> 1. Return the result applying the legacy **LOAD\_AS\_DIRECTORY**
1122-
> CommonJS resolver to _packageURL_, throwing a _Module Not Found_
1123-
> error for no resolution.
1124-
> 7. Otherwise,
1125-
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
1126-
> 11. Throw a _Module Not Found_ error.
1111+
> 10. If _selfUrl_ is not **undefined**, return _selfUrl_.
1112+
> 11. While _parentURL_ is not the file system root,
1113+
> 1. Let _packageURL_ be the URL resolution of _"node_modules/"_
1114+
> concatenated with _packageSpecifier_, relative to _parentURL_.
1115+
> 2. Set _parentURL_ to the parent folder URL of _parentURL_.
1116+
> 3. If the folder at _packageURL_ does not exist, then
1117+
> 1. Continue the next loop iteration.
1118+
> 4. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1119+
> 5. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
1120+
> **undefined**, then
1121+
> 1. Return the result of **PACKAGE\_EXPORTS\_RESOLVE**(_packageURL_,
1122+
> _packageSubpath_, _pjson.exports_, _defaultConditions_).
1123+
> 6. Otherwise, if _packageSubpath_ is equal to _"."_, then
1124+
> 1. If _pjson.main_ is a string, then
1125+
> 1. Return the URL resolution of _main_ in _packageURL_.
1126+
> 7. Otherwise,
1127+
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
1128+
> 12. Throw a _Module Not Found_ error.
11271129

11281130
**PACKAGE\_SELF\_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_)
11291131

@@ -1253,18 +1255,20 @@ _internal_, _conditions_)
12531255
> _"/"_ and is not a valid URL, then
12541256
> 1. If _pattern_ is **true**, then
12551257
> 1. Return **PACKAGE\_RESOLVE**(_target_ with every instance of
1256-
> _"\*"_ replaced by _subpath_, _packageURL_ + _"/"_)\_.
1258+
> _"\*"_ replaced by _subpath_, _packageURL_ + _"/"_).
12571259
> 2. Return **PACKAGE\_RESOLVE**(_target_ + _subpath_,
1258-
> _packageURL_ + _"/"_)\_.
1260+
> _packageURL_ + _"/"_)_.
12591261
> 2. Otherwise, throw an _Invalid Package Target_ error.
12601262
> 3. If _target_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
1261-
> _"node\_modules"_ segments after the first segment, throw an
1262-
> _Invalid Package Target_ error.
1263+
> _"node\_modules"_ segments after the first segment, case insensitive and
1264+
> including percent encoded variants, throw an _Invalid Package Target_
1265+
> error.
12631266
> 4. Let _resolvedTarget_ be the URL resolution of the concatenation of
12641267
> _packageURL_ and _target_.
12651268
> 5. Assert: _resolvedTarget_ is contained in _packageURL_.
12661269
> 6. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
1267-
> _"node\_modules"_ segments, throw an _Invalid Module Specifier_ error.
1270+
> _"node\_modules"_ segments, case insensitive and including percent
1271+
> encoded variants, throw an _Invalid Module Specifier_ error.
12681272
> 7. If _pattern_ is **true**, then
12691273
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
12701274
> _"\*"_ replaced with _subpath_.
@@ -1297,19 +1301,21 @@ _internal_, _conditions_)
12971301
> 4. Otherwise, if _target_ is _null_, return **null**.
12981302
> 5. Otherwise throw an _Invalid Package Target_ error.
12991303

1300-
**ESM\_FORMAT**(_url_)
1304+
**ESM\_FILE\_FORMAT**(_url_)
13011305

13021306
> 1. Assert: _url_ corresponds to an existing file.
13031307
> 2. Let _pjson_ be the result of **READ\_PACKAGE\_SCOPE**(_url_).
13041308
> 3. If _url_ ends in _".mjs"_, then
13051309
> 1. Return _"module"_.
13061310
> 4. If _url_ ends in _".cjs"_, then
13071311
> 1. Return _"commonjs"_.
1308-
> 5. If _pjson?.type_ exists and is _"module"_, then
1312+
> 5. If _url_ ends in _".json"_, then
1313+
> 1. Return _"json"_.
1314+
> 6. If _pjson?.type_ exists and is _"module"_, then
13091315
> 1. If _url_ ends in _".js"_, then
13101316
> 1. Return _"module"_.
13111317
> 2. Throw an _Unsupported File Extension_ error.
1312-
> 6. Otherwise,
1318+
> 7. Otherwise,
13131319
> 1. Throw an _Unsupported File Extension_ error.
13141320

13151321
**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,
@@ -384,9 +385,10 @@ const encodedSepRegEx = /%2F|%5C/i;
384385
/**
385386
* @param {URL} resolved
386387
* @param {string | URL | undefined} base
388+
* @param {boolean} preserveSymlinks
387389
* @returns {URL | undefined}
388390
*/
389-
function finalizeResolution(resolved, base) {
391+
function finalizeResolution(resolved, base, preserveSymlinks) {
390392
if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname))
391393
throw new ERR_INVALID_MODULE_SPECIFIER(
392394
resolved.pathname, 'must not include encoded "/" or "\\" characters',
@@ -417,6 +419,17 @@ function finalizeResolution(resolved, base) {
417419
path || resolved.pathname, base && fileURLToPath(base), 'module');
418420
}
419421

422+
if (!preserveSymlinks) {
423+
const real = realpathSync(path, {
424+
[internalFS.realpathCacheKey]: realpathCache
425+
});
426+
const { search, hash } = resolved;
427+
resolved =
428+
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));
429+
resolved.search = search;
430+
resolved.hash = hash;
431+
}
432+
420433
return resolved;
421434
}
422435

@@ -468,7 +481,8 @@ function throwInvalidPackageTarget(
468481
internal, base && fileURLToPath(base));
469482
}
470483

471-
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
484+
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;
485+
const invalidPackageNameRegEx = /^\.|%|\\/;
472486
const patternRegEx = /\*/g;
473487

474488
function resolvePackageTargetString(
@@ -810,13 +824,9 @@ function parsePackageName(specifier, base) {
810824
specifier : StringPrototypeSlice(specifier, 0, separatorIndex);
811825

812826
// Package name cannot have leading . and cannot have percent-encoding or
813-
// separators.
814-
for (let i = 0; i < packageName.length; i++) {
815-
if (packageName[i] === '%' || packageName[i] === '\\') {
816-
validPackageName = false;
817-
break;
818-
}
819-
}
827+
// \\ separators.
828+
if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null)
829+
validPackageName = false;
820830

821831
if (!validPackageName) {
822832
throw new ERR_INVALID_MODULE_SPECIFIER(
@@ -836,6 +846,9 @@ function parsePackageName(specifier, base) {
836846
* @returns {URL}
837847
*/
838848
function packageResolve(specifier, base, conditions) {
849+
if (NativeModule.canBeRequiredByUsers(specifier))
850+
return new URL('node:' + specifier);
851+
839852
const { packageName, packageSubpath, isScoped } =
840853
parsePackageName(specifier, base);
841854

@@ -912,9 +925,10 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
912925
* @param {string} specifier
913926
* @param {string | URL | undefined} base
914927
* @param {Set<string>} conditions
928+
* @param {boolean} preserveSymlinks
915929
* @returns {URL}
916930
*/
917-
function moduleResolve(specifier, base, conditions) {
931+
function moduleResolve(specifier, base, conditions, preserveSymlinks) {
918932
// Order swapped from spec for minor perf gain.
919933
// Ok since relative URLs cannot parse as URLs.
920934
let resolved;
@@ -929,7 +943,9 @@ function moduleResolve(specifier, base, conditions) {
929943
resolved = packageResolve(specifier, base, conditions);
930944
}
931945
}
932-
return finalizeResolution(resolved, base);
946+
if (resolved.protocol !== 'file:')
947+
return resolved;
948+
return finalizeResolution(resolved, base, preserveSymlinks);
933949
}
934950

935951
/**
@@ -1001,28 +1017,6 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10011017
}
10021018
}
10031019
}
1004-
let parsed;
1005-
try {
1006-
parsed = new URL(specifier);
1007-
if (parsed.protocol === 'data:') {
1008-
return {
1009-
url: specifier
1010-
};
1011-
}
1012-
} catch {}
1013-
if (parsed && parsed.protocol === 'node:')
1014-
return { url: specifier };
1015-
if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
1016-
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed);
1017-
if (NativeModule.canBeRequiredByUsers(specifier)) {
1018-
return {
1019-
url: 'node:' + specifier
1020-
};
1021-
}
1022-
if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) {
1023-
// This is gonna blow up, we want the error
1024-
new URL(specifier, parentURL);
1025-
}
10261020

10271021
const isMain = parentURL === undefined;
10281022
if (isMain) {
@@ -1041,7 +1035,8 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10411035
conditions = getConditionsSet(conditions);
10421036
let url;
10431037
try {
1044-
url = moduleResolve(specifier, parentURL, conditions);
1038+
url = moduleResolve(specifier, parentURL, conditions,
1039+
isMain ? preserveSymlinksMain : preserveSymlinks);
10451040
} catch (error) {
10461041
// Try to give the user a hint of what would have been the
10471042
// resolved CommonJS module
@@ -1065,17 +1060,9 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10651060
throw error;
10661061
}
10671062

1068-
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
1069-
const urlPath = fileURLToPath(url);
1070-
const real = realpathSync(urlPath, {
1071-
[internalFS.realpathCacheKey]: realpathCache
1072-
});
1073-
const old = url;
1074-
url = pathToFileURL(
1075-
real + (StringPrototypeEndsWith(urlPath, sep) ? '/' : ''));
1076-
url.search = old.search;
1077-
url.hash = old.hash;
1078-
}
1063+
if (url.protocol !== 'file:' && url.protocol !== 'data:' &&
1064+
url.protocol !== 'node:')
1065+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
10791066

10801067
return { url: `${url}` };
10811068
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
116116
// Even though 'pkgexports/sub/asdf.js' works, alternate "path-like"
117117
// variants do not to prevent confusion and accidental loopholes.
118118
['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'],
119+
// Cannot reach into node_modules, even percent encoded
120+
['pkgexports/sub/no%64e_modules', './sub/no%64e_modules'],
121+
// Cannot backtrack below exposed path, even with percent encoded "."
122+
['pkgexports/sub/%2e./asdf', './asdf'],
119123
]);
120124

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

0 commit comments

Comments
 (0)