high · 7.1Jun 27, 2026

pnpm Hoisted Lockfile Alias Path Traversal ()

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packagepnpm
Ecosystemnpm
Affected< 10.34.4
Fixed in10.34.4

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

text
# 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.0

The 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.

Reporter not attributed.

References: [1][2]