pnpm Hoisted Lockfile Alias Path Traversal ()
A crafted pnpm lockfile alias used with the hoisted node linker could escape node_modules entirely, writing package files to arbitrary paths on the filesystem.
The problem
When pnpm installs with nodeLinker: hoisted, it builds a dependency graph from the lockfile and joins each alias name directly under the target modules directory using path.join(modules, dep.name). No validation of dep.name was performed at this graph sink.
A traversal alias such as ../../../etc or ../../.git/hooks would resolve cleanly outside node_modules. Reserved names like .bin or .pnpm would overwrite pnpm-owned layout directories. An attacker who can supply a crafted lockfile (e.g. via a compromised dependency or repository) triggers arbitrary file writes on the victim's machine at pnpm install time.
Proof of concept
# Crafted pnpm-lock.yaml snippet (lockfileVersion 9, hoisted nodeLinker)
# Place this in a project with nodeLinker: hoisted in pnpm-workspace.yaml
# then run: pnpm install --frozen-lockfile
lockfileVersion: '9'
importers:
.:
dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21
# Traversal alias: resolves to <project>/../../../tmp/pwned/
../../../tmp/pwned:
specifier: npm:lodash@4.17.21
version: 4.17.21
# Effect before patch:
# lockfileToHoistedDepGraph builds dir = path.join(node_modules, '../../../tmp/pwned')
# => resolves to /tmp/pwned
# pnpm then imports/symlinks the package files there.
# Reserved alias variant (overwrites pnpm-owned layout):
# .bin:
# specifier: npm:evil@1.0.0
# version: 1.0.0The vulnerable sink was in installing/deps-restorer/src/lockfileToHoistedDepGraph.ts, which called path.join(modules, dep.name) for every node in the hoisted graph without first validating dep.name. The path.join call does not block traversal segments, so ../../../escape resolves cleanly to a directory outside node_modules.
The patch introduced a shared helper, fs/symlink-dependency/src/safeJoinModulesDir.ts, that rejects traversal segments, absolute paths, platform-specific separators, and reserved pnpm names (.bin, .pnpm, node_modules) before any graph insertion or filesystem work.
The same containment rule was mirrored in the Rust implementation (pacquet). Both implementations now return ERR_PNPM_INVALID_DEPENDENCY_NAME on a bad alias.
Root cause: CWE-22 (Path Traversal) combined with CWE-73 (External Control of File Name or Path). The lockfile is a repository-committed, user-controlled artifact, so any consumer of the lockfile is a potential vector.
The fix
Upgrade pnpm to 10.34.4 or later (pnpm 11.x users: 11.7.0 or later also carries the fix). No configuration change is required. After upgrading, pnpm install will reject lockfiles containing traversal or reserved aliases with ERR_PNPM_INVALID_DEPENDENCY_NAME before any fetch or filesystem work occurs.