Skip to content

Commit 448b203

Browse files
committed
arborist: move packageName sanitization into IsolatedNode getter
Per maintainer feedback, add a packageName getter to IsolatedNode that derives the name from the filesystem path via @npmcli/name-from-folder, preventing path-like node.name values from introducing traversal. Simplify #assignCommonProperties to defer to node.packageName. Add tests for path traversal, scoped packages, and fallback behavior.
1 parent 2bcad9e commit 448b203

File tree

3 files changed

+71
-1
lines changed

3 files changed

+71
-1
lines changed

workspaces/arborist/lib/arborist/isolated-reifier.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ module.exports = cls => class IsolatedReifier extends cls {
178178
result.id = this.counter++
179179
/* istanbul ignore next - packageName is always set for real packages */
180180
result.name = result.isWorkspace ? (node.packageName || node.name) : node.name
181-
result.packageName = nameFromFolder(node.path) || node.packageName || node.name
181+
result.packageName = node.packageName || nameFromFolder(node.path)
182182
result.package = { ...node.package }
183183
result.package.bundleDependencies = undefined
184184

workspaces/arborist/lib/isolated-classes.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Alternate versions of different classes that we use for isolated mode
22
const CaseInsensitiveMap = require('./case-insensitive-map.js')
33
const { resolve } = require('node:path')
4+
const nameFromFolder = require('@npmcli/name-from-folder')
45

56
// fake lib/inventory.js
67
class IsolatedInventory extends Map {
@@ -104,6 +105,10 @@ class IsolatedNode {
104105
return !!(hasInstallScript || install || preinstall || postinstall)
105106
}
106107

108+
get packageName () {
109+
return nameFromFolder(this.path) || this.name
110+
}
111+
107112
get version () {
108113
return this.package.version
109114
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const t = require('tap')
2+
const path = require('node:path')
3+
const { IsolatedNode, IsolatedLink } = require('../lib/isolated-classes.js')
4+
5+
t.test('IsolatedNode packageName getter', t => {
6+
t.test('returns name extracted from path', t => {
7+
const node = new IsolatedNode({
8+
path: path.join('/some', 'node_modules', 'my-package'),
9+
name: 'my-package',
10+
package: { version: '1.0.0' },
11+
})
12+
t.equal(node.packageName, 'my-package')
13+
t.end()
14+
})
15+
16+
t.test('sanitizes path-like node name — path traversal via name', t => {
17+
// Simulates a pretend node where node.name is a relative/path-like value.
18+
// packageName must return only the package name, not the traversal path.
19+
const node = new IsolatedNode({
20+
path: path.join('/some', 'node_modules', 'my-package'),
21+
name: '../my-package',
22+
package: { version: '1.0.0' },
23+
})
24+
t.equal(node.packageName, 'my-package', 'extracts name from path, ignores path-like node.name')
25+
t.end()
26+
})
27+
28+
t.test('handles scoped packages', t => {
29+
const node = new IsolatedNode({
30+
path: path.join('/some', 'node_modules', '@scope', 'pkg'),
31+
name: '@scope/pkg',
32+
package: { version: '2.0.0' },
33+
})
34+
t.equal(node.packageName, '@scope/pkg')
35+
t.end()
36+
})
37+
38+
t.test('falls back to name when path is not set', t => {
39+
const node = new IsolatedNode({
40+
name: 'fallback-name',
41+
package: { version: '1.0.0' },
42+
})
43+
t.equal(node.packageName, 'fallback-name', 'falls back to this.name when path is absent')
44+
t.end()
45+
})
46+
47+
t.end()
48+
})
49+
50+
t.test('IsolatedLink inherits packageName getter from IsolatedNode', t => {
51+
const target = new IsolatedNode({
52+
path: path.join('/some', 'node_modules', 'pkg'),
53+
name: 'pkg',
54+
package: { version: '1.0.0' },
55+
})
56+
const link = new IsolatedLink({
57+
path: path.join('/some', 'node_modules', '.store', 'pkg@1.0.0', 'node_modules', 'pkg'),
58+
name: 'pkg',
59+
package: { version: '1.0.0' },
60+
realpath: target.path,
61+
target,
62+
})
63+
t.equal(link.packageName, 'pkg')
64+
t.end()
65+
})

0 commit comments

Comments
 (0)