GitHub Advisoryadvisory · npm

GHSA-c59q-g84q-2gj5 pnpm: Virtual store linker path traversal via unvalidated depPath name in lockfileToDepGraph

HIGH Published 2026-09-02 Modified 2026-09-02 Official page ↗
Aliases
CVE-2026-82392
Ecosystem
npm
Packages
pnpm
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L
Weaknesses
CWE-22
Reviewed
2026-09-02

Affected packages

• npm pnpm: < 10.34.5 → fixed in 10.34.5 • npm pnpm: >= 11.0.0, < 11.11.0 → fixed in 11.11.0

Description

Summary The virtual store linker constructs package installation directories using `path.join(modules, pkgName)` where `pkgName` is extracted from lockfile `packages` keys via `dp.parse(depPath).name` without validation. A crafted `pnpm-lock.yaml` with traversal sequences in depPath keys (e.g., `../../../tmp/[email protected]`) causes package content to be written to arbitrary filesystem paths during `pnpm install`. This is an incomplete fix of GHSA-fr4h-3cph-29xv — the `safeJoinModulesDir` containment helper was applied to the hoisted linker and `symlinkDependency` but NOT to the virtual store linker's `lockfileToDepGraph.ts:233`. Details Root Cause `dp.parse()` at `pnpm11/deps/path/src/index.ts:135` extracts the package name as: ```typescript const name = dependencyPath.substring(0, sepIndex) ``` This is a raw substring operation with zero validation that `name` is a valid npm package name. A depPath of `../../../tmp/[email protected]` yields `name = '../../../tmp/pwned'`. Vulnerable Code Path 1. `pnpm-lock.yaml` → `lockfile.packages['../../../../../../../tmp/[email protected]']` (attacker-controlled lockfile key) 2. `nameVerFromPkgSnapshot(depPath, pkgSnapshot)` at `lockfile/utils/src/nameVerFromPkgSnapshot.ts:16` → calls `dp.parse(depPath)` → returns `{ name: '../../../../../../../tmp/pwned' }` 3. `lockfileToDepGraph.ts:232` → `modules = path.join(dirInVirtualStore, 'node_modules')` 4. `lockfileToDepGraph.ts:233` → `dir = path.join(modules, pkgName)` → resolves to `/tmp/pwned` (ESCAPES virtual store) 5. `storeController.importPackage(depNode.dir, ...)` → writes package content to the traversed path Why Existing Defenses Don't Catch It • `depPathToFilename()` — replaces `/` with `+` for the `dirInVirtualStore` path, but `pkgName` comes SEPARATELY from `dp.parse()` and is NOT passed through this function • `verifyLockfileResolutions()` — validates dependency map keys (aliases) via `isValidDependencyAlias()`, but never validates the depPath keys themselves • Lockfile parser — `yaml.load(lockfileRawContent)` with no schema validation on `packages` keys • `importPackage()` — accepts `targetDir` and passes it directly to `cafsStore.importPackage(targetDir, ...)` with zero containment check • Integrity verification — requires a real fetchable package but does not validate the destination path Escalation to RCE (non-default config) When `dangerouslyAllowAllBuilds: true` is configured (or the traversal package name is in the explicit `allowBuilds` list), the same traversed path is used in the rebuild phase at `after-install/src/index.ts:402,470`. The attacker's `postinstall` script then executes with the victim's shell access. Under default config, `allowBuild` returns false for unknown packages, limiting impact to arbitrary file write. Also Affected (PnP linker) When `nodeLinker: pnp` is configured, `lockfileToPackageRegistry()` at `lockfile/to-pnp/src/index.ts:105-110` uses the same unvalidated `dp.parse().name` in `packageLocation` construction, allowing the `.pnp.cjs` resolver map to point outside the virtual store. This is a lower-impact variant (PnP is not the default linker). Impact An attacker who can commit a crafted `pnpm-lock.yaml` to a repository (or supply one via a malicious package) can cause arbitrary file writes on the machine of any user who runs `pnpm install`. Written content is the actual package files from a real npm package (attacker controls which package and which destination). Targets for arbitrary file write include: • `.git/hooks/pre-commit` — code execution on next git operation • `~/.local/bin/` — binary hijacking • Project source files — supply chain injection Reproduction Craft a `pnpm-lock.yaml`: ```yaml lockfileVersion: '9.0' packages: ../../../../../../../tmp/[email protected]: resolution: {integrity: sha512-} engines: {node: '>=14'} snapshots: ../../../../../../../tmp/[email protected]: {} importers: .: dependencies: legitimate-name: specifier: ^1.0.0 version: ../../../../../../../tmp/[email protected] ``` Run `pnpm install` — package content is written to `/tmp/pwned/` instead of the virtual store. Recommended Fix Apply `safeJoinModulesDir` (or equivalent validation) at: • `lockfileToDepGraph.ts:233` — `path.join(modules, pkgName)` • `after-install/src/index.ts:402` — `path.join(pkgModulesDir(depPath), pkgInfo.name)` • `lockfile/to-pnp/src/index.ts:105-110` — PnP `packageLocation` Alternatively, validate depPath keys during lockfile parsing to reject any that don't produce valid npm package names via `dp.parse()`. Relationship to GHSA-fr4h-3cph-29xv GHSA-fr4h-3cph-29xv fixed the hoisted linker path (`lockfileToHoistedDepGraph.ts:222`) by adding `safeJoinModulesDir`. The same fix was NOT applied to the virtual store linker, which uses the identical `dp.parse().name → path.join()` pattern at `lockfileToDepGraph.ts:233`.

Related

Alias · CVE (1)

Weakness · CWE (1)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness

References