GHSA-c59q-g84q-2gj5: Path Traversal

Published Sep 2, 2026
·
Updated

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/pwned@1.0.0) 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/pwned@1.0.0 yields name = '../../../tmp/pwned'.

Vulnerable Code Path

1. pnpm-lock.yaml → lockfile.packages['../../../../../../../tmp/pwned@1.0.0'] (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, 'nodemodules') 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/pwned@1.0.0: resolution: {integrity: sha512-<real-package-integrity>} engines: {node: '>=14'} snapshots: ../../../../../../../tmp/pwned@1.0.0: {} importers: .: dependencies: legitimate-name: specifier: ^1.0.0 version: ../../../../../../../tmp/pwned@1.0.0

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.

Affected Software

2 affected componentsFixes available
npm/pnpm>=11.0.0<11.11.0
11.11.0
npm/pnpm<10.34.5
10.34.5

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/pnpm to a version that resolves this vulnerability.

    Fixed in 11.11.0
  2. Upgrade

    Upgrade npm/pnpm to a version that resolves this vulnerability.

    Fixed in 10.34.5

Event History

Sep 2, 2026
Advisory Published
via GitHub·02:37 PM
Data Sourced
via GitHub·02:37 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Users who run pnpm install against a repository containing an attacker-crafted pnpm-lock.yaml are exposed. Exploitation requires user interaction because the victim must run the install command with the malicious lockfile present.

2

What does an attacker need to provide?

The attacker needs to control or induce use of a pnpm-lock.yaml containing crafted packages keys with traversal sequences in the dependency path, such as ../../../tmp/pwned@1.0.0. No authentication or existing privileges are required according to the provided vector.

3

How can I check whether a lockfile may be malicious?

Inspect pnpm-lock.yaml package keys for dependency paths whose package-name portion includes traversal components such as ../ or paths that resolve outside the expected modules directory. Affected installs can write package content to filesystem locations selected through those crafted paths.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203