Skip to content

Commit 075cfbf

Browse files
guybedfordtargos
authored andcommitted
module: resolver & spec hardening /w refactoring
PR-URL: #40510 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 8a00dc5 commit 075cfbf

3 files changed

Lines changed: 88 additions & 89 deletions

File tree

‎doc/api/esm.md‎

Lines changed: 52 additions & 44 deletions
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 URLof _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 URLof _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‎

Lines changed: 32 additions & 45 deletions
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-
functionfinalizeResolution(resolved,base){
367+
functionfinalizeResolution(resolved,base,preserveSymlinks){
366368
if(RegExpPrototypeTest(encodedSepRegEx,resolved.pathname))
367369
thrownewERR_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+
constreal=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
returnresolved;
397410
}
398411

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

447-
constinvalidSegmentRegEx=/(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
460+
constinvalidSegmentRegEx=/(^|\\|\/)((\.|%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+
constinvalidPackageNameRegEx=/^\.|%|\\/;
448462
constpatternRegEx=/\*/g;
449463

450464
functionresolvePackageTargetString(
@@ -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(leti=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
thrownewERR_INVALID_MODULE_SPECIFIER(
@@ -803,6 +813,9 @@ function parsePackageName(specifier, base) {
803813
* @returns {URL}
804814
*/
805815
functionpackageResolve(specifier,base,conditions){
816+
if(NativeModule.canBeRequiredByUsers(specifier))
817+
returnnewURL('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-
functionmoduleResolve(specifier,base,conditions){
898+
functionmoduleResolve(specifier,base,conditions,preserveSymlinks){
885899
// Order swapped from spec for minor perf gain.
886900
// Ok since relative URLs cannot parse as URLs.
887901
letresolved;
@@ -896,7 +910,9 @@ function moduleResolve(specifier, base, conditions) {
896910
resolved=packageResolve(specifier,base,conditions);
897911
}
898912
}
899-
returnfinalizeResolution(resolved,base);
913+
if(resolved.protocol!=='file:')
914+
returnresolved;
915+
returnfinalizeResolution(resolved,base,preserveSymlinks);
900916
}
901917

902918
/**
@@ -968,28 +984,6 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
968984
}
969985
}
970986
}
971-
letparsed;
972-
try{
973-
parsed=newURL(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-
thrownewERR_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-
newURL(specifier,parentURL);
992-
}
993987

994988
constisMain=parentURL===undefined;
995989
if(isMain){
@@ -1008,7 +1002,8 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10081002
conditions=getConditionsSet(conditions);
10091003
leturl;
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
throwerror;
10331028
}
10341029

1035-
if(isMain ? !preserveSymlinksMain : !preserveSymlinks){
1036-
consturlPath=fileURLToPath(url);
1037-
constreal=realpathSync(urlPath,{
1038-
[internalFS.realpathCacheKey]: realpathCache
1039-
});
1040-
constold=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+
thrownewERR_UNSUPPORTED_ESM_URL_SCHEME(url);
10461033

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

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

Lines changed: 4 additions & 0 deletions
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]ofundefinedExports){

0 commit comments

Comments
 (0)