GHSA-vq4v-j7r6-jq4m: Path Traversal
Summary When resolving a package, pnpm uses the resolved manifest name as a raw path segment for the isolated-linker import target. A tarball dependency whose package.json name is a scoped path traversal (@x/../../…/<abs path>) is therefore extracted outside nodemodules, to an attacker-chosen absolute path, and can overwrite existing files there. Attacker controls the destination, filenames, and contents → arbitrary file write → code execution (e.g. ~/.zshrc, .git/hooks/pre-commit, another package's code). Occurs during pnpm install even with --ignore-scripts (no lifecycle scripts run), defeating that safety.
Same class as the just-patched GHSA-hwx4 (transitive-dependency alias traversal) and GHSA-v23m (stage download manifest name/version traversal), in a sink their fixes did not cover: the isolated-linker import target keyed by the resolved name.
Root cause - The isolated-linker import target is built with a raw path.join(modules, <resolved name>) in installing/deps-resolver/src/resolvePeers.ts:706, installing/deps-resolver/src/index.ts:614, and deps/graph-builder/src/lockfileToDepGraph.ts:233 — without the safeJoinModulesDir guard used on the symlink/hoisted/bin paths (installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222). The store location is nodemodules/.pnpm/<id>/nodemodules/<name>, so a traversal <name> escapes. - The only resolve-time name gate (resolving/npm-resolver/src/pickPackage.ts:753) rejects only unscoped names containing /, so a scoped @x/../.. passes.
Steps to reproduce Self-contained PoC (real pnpm@11.9.0; loopback tarball server; escape target is a throwaway temp dir): npm i pnpm@11.9.0 host a tarball whose package.json name = "@x/"+"../".repeat(25)+"<abs>/OUTSIDE"; victim depends on the http URL pnpm install --ignore-scripts Confirmed output (repro/poc.mjs, exit 0): escape dir is outside the project : true new file implanted outside nodemodules : true pre-existing file OVERWRITTEN : true CONFIRMED: a tarball dependency wrote & overwrote files OUTSIDE the project during pnpm install --ignore-scripts
Remediation Route the isolated-linker import-target joins (resolvePeers.ts:706, deps-resolver/index.ts:614, lockfileToDepGraph.ts:233) through safeJoinModulesDir (as the hoisted linker already does), and/or enforce validate-npm-package-name on the resolved manifest name (close the scoped-name gap at pickPackage.ts:753) so the import target rejects a traversal name and re-asserts containment before any write.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/pnpmto a version that resolves this vulnerability.Fixed in 11.11.0 - Upgrade
Upgrade
npm/pnpmto a version that resolves this vulnerability.Fixed in 10.34.5 - Configuration
Route the isolated-linker import-target joins in resolvePeers.ts:706, installing/deps-resolver/src/index.ts:614, and deps/graph-builder/src/lockfileToDepGraph.ts:233 through safeJoinModulesDir (matching the guard used for symlink/hoisted/bin paths in installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222) so traversal in resolved names cannot escape node_modules.
pnpm isolated-linker dependency import target (installing/deps-resolver/src/resolvePeers.ts:706; installing/deps-resolver/src/index.ts:614; deps/graph-builder/src/lockfileToDepGraph.ts:233) safeJoinModulesDir guard = use safeJoinModulesDir for join(modules, <resolved name>) instead of raw path.join(modules, <resolved name>) - Configuration
Close the scoped-name gap by enforcing validate-npm-package-name on the resolved manifest name (the only current gate in pickPackage.ts:753 rejects only unscoped names containing '/', so scoped traversal names pass). Reject traversal-containing resolved manifest names before using them as import-target path segments.
pnpm package name validation at resolve time (resolving/npm-resolver/src/pickPackage.ts:753) validate-npm-package-name gate = enforce validate-npm-package-name (or equivalent) on the resolved manifest name to reject scoped traversal names like @x/../../…/<abs path> - Compensating control
As a containment measure, prevent pnpm from resolving/extracting attacker-controlled tarballs whose package.json name contains traversal (e.g., scoped path traversal like @x/../../…/<abs path>), for example by restricting allowed dependency sources or validating tarball package.json names in your supply chain before running pnpm install.
Event History
Frequently Asked Questions
Does using --ignore-scripts prevent exploitation?
No. The traversal occurs during pnpm install while resolving and extracting the dependency, even when lifecycle scripts are disabled with --ignore-scripts.
What does an attacker need to provide to trigger the issue?
A tarball dependency whose package.json manifest name is a crafted scoped path-traversal value, such as one containing @x/../../ followed by an absolute path. The attacker-controlled manifest name is used as part of the isolated-linker import destination.
What could an attacker overwrite if exploitation succeeds?
The attacker controls the destination path, filenames, and file contents, enabling writes to an attacker-chosen absolute path outside node_modules. Examples given include shell startup files, Git hooks, or another package's code, which can lead to code execution.