diff --git a/lib/internal/modules/package_map.js b/lib/internal/modules/package_map.js index 38e7e12206c9..f5468390cd91 100644 --- a/lib/internal/modules/package_map.js +++ b/lib/internal/modules/package_map.js @@ -4,8 +4,10 @@ const { JSONParse, ObjectEntries, SafeMap, + StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeSlice, + StringPrototypeStartsWith, } = primordials; const assert = require('internal/assert'); @@ -19,6 +21,7 @@ const getPackageMapPath = getLazy(() => const fs = require('fs'); const { + ERR_INVALID_MODULE_SPECIFIER, ERR_PACKAGE_MAP_EXTERNAL_FILE, ERR_PACKAGE_MAP_INVALID, ERR_PACKAGE_MAP_KEY_NOT_FOUND, @@ -193,6 +196,18 @@ class PackageMap { throw new ERR_PACKAGE_MAP_KEY_NOT_FOUND(targetKey, this.#configPath); } + if (subpath !== '.') { + let packageBaseURL = pathToFileURL(targetEntry.path); + if (!StringPrototypeEndsWith(packageBaseURL.pathname, '/')) { + packageBaseURL = new URL(`${packageBaseURL.href}/`); + } + const resolved = new URL(subpath, packageBaseURL); + + if (!StringPrototypeStartsWith(resolved.pathname, packageBaseURL.pathname)) { + throw new ERR_INVALID_MODULE_SPECIFIER(specifier, 'resolves outside its package', parentPath); + } + } + return { packagePath: targetEntry.path, subpath }; } } diff --git a/test/fixtures/package-map/traversal/app/index.mjs b/test/fixtures/package-map/traversal/app/index.mjs new file mode 100644 index 000000000000..5519147ba5af --- /dev/null +++ b/test/fixtures/package-map/traversal/app/index.mjs @@ -0,0 +1 @@ +export default 'app'; diff --git a/test/fixtures/package-map/traversal/dep/sub/file.cjs b/test/fixtures/package-map/traversal/dep/sub/file.cjs new file mode 100644 index 000000000000..6aa967e0f0ae --- /dev/null +++ b/test/fixtures/package-map/traversal/dep/sub/file.cjs @@ -0,0 +1 @@ +module.exports = 'dep-sub-cjs'; diff --git a/test/fixtures/package-map/traversal/dep/sub/file.mjs b/test/fixtures/package-map/traversal/dep/sub/file.mjs new file mode 100644 index 000000000000..d41f326f0899 --- /dev/null +++ b/test/fixtures/package-map/traversal/dep/sub/file.mjs @@ -0,0 +1 @@ +export default 'dep-sub'; diff --git a/test/fixtures/package-map/traversal/map.json b/test/fixtures/package-map/traversal/map.json new file mode 100644 index 000000000000..0220f099cf40 --- /dev/null +++ b/test/fixtures/package-map/traversal/map.json @@ -0,0 +1,6 @@ +{ + "packages": { + "app": { "url": "./app", "dependencies": { "dep": "dep" } }, + "dep": { "url": "./dep", "dependencies": {} } + } +} diff --git a/test/fixtures/package-map/traversal/secret.cjs b/test/fixtures/package-map/traversal/secret.cjs new file mode 100644 index 000000000000..49563583c939 --- /dev/null +++ b/test/fixtures/package-map/traversal/secret.cjs @@ -0,0 +1 @@ +module.exports = 'SECRET-OUTSIDE-CJS'; diff --git a/test/fixtures/package-map/traversal/secret.mjs b/test/fixtures/package-map/traversal/secret.mjs new file mode 100644 index 000000000000..a9c45fdb3b79 --- /dev/null +++ b/test/fixtures/package-map/traversal/secret.mjs @@ -0,0 +1 @@ +export default 'SECRET-OUTSIDE'; diff --git a/test/parallel/test-package-map-traversal.mjs b/test/parallel/test-package-map-traversal.mjs new file mode 100644 index 000000000000..d63a3f120d92 --- /dev/null +++ b/test/parallel/test-package-map-traversal.mjs @@ -0,0 +1,69 @@ +import '../common/index.mjs'; +import { spawnSyncAndAssert, spawnSyncAndExit } from '../common/child_process.js'; +import * as fixtures from '../common/fixtures.mjs'; + +const map = fixtures.path('package-map/traversal/map.json'); +const cwd = fixtures.path('package-map/traversal/app'); + +function runESM(specifier) { + return [ + '--no-warnings', + '--experimental-package-map', map, + '--input-type=module', + '--eval', `import x from '${specifier}'; console.log(x);`, + ]; +} + +function runCJS(specifier) { + return [ + '--no-warnings', + '--experimental-package-map', map, + '--eval', `console.log(require('${specifier}'));`, + ]; +} + +for (const specifier of [ + 'dep/../secret.mjs', + 'dep/%2e%2e/secret.mjs', + 'dep/sub/../../secret.mjs', +]) { + spawnSyncAndExit(process.execPath, runESM(specifier), { cwd }, { + status: 1, + signal: null, + stderr: /ERR_INVALID_MODULE_SPECIFIER/, + }); +} + +for (const specifier of [ + 'dep/sub/file.mjs', + 'dep/./sub/file.mjs', + 'dep/sub/../sub/file.mjs', +]) { + spawnSyncAndAssert(process.execPath, runESM(specifier), { cwd }, { + stdout: /dep-sub/, + trim: true, + }); +} + +for (const specifier of [ + 'dep/../secret.cjs', + 'dep/%2e%2e/secret.cjs', + 'dep/sub/../../secret.cjs', +]) { + spawnSyncAndExit(process.execPath, runCJS(specifier), { cwd }, { + status: 1, + signal: null, + stderr: /ERR_INVALID_MODULE_SPECIFIER/, + }); +} + +for (const specifier of [ + 'dep/sub/file.cjs', + 'dep/./sub/file.cjs', + 'dep/sub/../sub/file.cjs', +]) { + spawnSyncAndAssert(process.execPath, runCJS(specifier), { cwd }, { + stdout: /dep-sub-cjs/, + trim: true, + }); +}