From a35d528756e78d15d37a971bd9a633e7805a5380 Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Thu, 19 Mar 2026 03:14:21 +0000 Subject: [PATCH 1/3] fix(arborist): sanitize packageName in path construction for linked strategy Node.packageName returns raw package.json name without validation (syncNormalize doesn't include fixName). This allows path traversal via malicious package names from file: deps or private registries. Changes: - Add packageName getter to IsolatedNode deriving safe names via nameFromFolder(path) - Wrap node.packageName through nameFromFolder in #assignCommonProperties so proxy objects get path-safe names - Fix hasShrinkwrap branch to use sanitized result.packageName instead of raw node.packageName in mkdirSync path construction --- workspaces/arborist/lib/arborist/isolated-reifier.js | 5 +++-- workspaces/arborist/lib/isolated-classes.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/workspaces/arborist/lib/arborist/isolated-reifier.js b/workspaces/arborist/lib/arborist/isolated-reifier.js index 121ad0effc8f6..5a2a5e1da53a9 100644 --- a/workspaces/arborist/lib/arborist/isolated-reifier.js +++ b/workspaces/arborist/lib/arborist/isolated-reifier.js @@ -4,6 +4,7 @@ const { join } = require('node:path') const { depth } = require('treeverse') const crypto = require('node:crypto') const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js') +const nameFromFolder = require('@npmcli/name-from-folder') // generate short hash key based on the dependency tree starting at this node const getKey = (startNode) => { @@ -149,7 +150,7 @@ module.exports = cls => class IsolatedReifier extends cls { node.root.path, 'node_modules', '.store', - `${node.packageName}@${node.version}` + `${result.packageName}@${node.version}` ) mkdirSync(dir, { recursive: true }) // TODO this approach feels wrong and shouldn't be necessary for shrinkwraps @@ -191,7 +192,7 @@ module.exports = cls => class IsolatedReifier extends cls { result.id = this.counter++ /* istanbul ignore next - packageName is always set for real packages */ result.name = result.isWorkspace ? (node.packageName || node.name) : node.name - result.packageName = node.packageName || node.name + result.packageName = nameFromFolder(node.packageName || node.path) result.package = { ...node.package } result.package.bundleDependencies = undefined diff --git a/workspaces/arborist/lib/isolated-classes.js b/workspaces/arborist/lib/isolated-classes.js index d9770a386791f..0a1f8681a1e97 100644 --- a/workspaces/arborist/lib/isolated-classes.js +++ b/workspaces/arborist/lib/isolated-classes.js @@ -1,6 +1,7 @@ // Alternate versions of different classes that we use for isolated mode const CaseInsensitiveMap = require('./case-insensitive-map.js') const { resolve } = require('node:path') +const nameFromFolder = require('@npmcli/name-from-folder') // fake lib/inventory.js class IsolatedInventory extends Map { @@ -104,6 +105,10 @@ class IsolatedNode { return !!(hasInstallScript || install || preinstall || postinstall) } + get packageName () { + return nameFromFolder(this.path) + } + get version () { return this.package.version } From 5c657fd148fe92e404aa9266da3cabce49a771a9 Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Thu, 19 Mar 2026 23:33:20 +0000 Subject: [PATCH 2/3] fix(arborist): add istanbul ignore for IsolatedNode packageName getter Match the existing pattern for emulation getters (getBundler, hasInstallScript) that are never directly tested. --- workspaces/arborist/lib/isolated-classes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/workspaces/arborist/lib/isolated-classes.js b/workspaces/arborist/lib/isolated-classes.js index 0a1f8681a1e97..8100b18b51d86 100644 --- a/workspaces/arborist/lib/isolated-classes.js +++ b/workspaces/arborist/lib/isolated-classes.js @@ -105,6 +105,7 @@ class IsolatedNode { return !!(hasInstallScript || install || preinstall || postinstall) } + /* istanbul ignore next -- emulate lib/node.js */ get packageName () { return nameFromFolder(this.path) } From ab15b25f492a8847ea926fdfceea90f558a1b0ed Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Fri, 20 Mar 2026 00:35:13 +0000 Subject: [PATCH 3/3] fix(arborist): align IsolatedNode.packageName with real Node behavior Per reviewer feedback, IsolatedNode.packageName should match the real Node class (return this.package.name) rather than always deriving from path. The sanitization via nameFromFolder already happens in #assignCommonProperties, so the getter doesn't need to duplicate it. --- workspaces/arborist/lib/isolated-classes.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workspaces/arborist/lib/isolated-classes.js b/workspaces/arborist/lib/isolated-classes.js index 8100b18b51d86..8e6e5b79a91f4 100644 --- a/workspaces/arborist/lib/isolated-classes.js +++ b/workspaces/arborist/lib/isolated-classes.js @@ -1,7 +1,6 @@ // Alternate versions of different classes that we use for isolated mode const CaseInsensitiveMap = require('./case-insensitive-map.js') const { resolve } = require('node:path') -const nameFromFolder = require('@npmcli/name-from-folder') // fake lib/inventory.js class IsolatedInventory extends Map { @@ -107,7 +106,7 @@ class IsolatedNode { /* istanbul ignore next -- emulate lib/node.js */ get packageName () { - return nameFromFolder(this.path) + return this.package.name || null } get version () {