OSV 1.4.0 · github-reviewed · 修改于 2026-09-02 22:37
发布时间
2026-09-02 22:37
GitHub 审查时间
2026-09-02 22:37
NVD 发布时间
2026-09-01 05:17
源文件
advisories/github-reviewed/2026/09/GHSA-c59q-g84q-2gj5/GHSA-c59q-g84q-2gj5.json
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.
dp.parse() at pnpm11/deps/path/src/index.ts:135 extracts the package name as:
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'.
pnpm-lock.yaml → lockfile.packages['../../../../../../../tmp/[email protected]'] (attacker-controlled lockfile key)nameVerFromPkgSnapshot(depPath, pkgSnapshot) at lockfile/utils/src/nameVerFromPkgSnapshot.ts:16 → calls dp.parse(depPath) → returns { name: '../../../../../../../tmp/pwned' }lockfileToDepGraph.ts:232 → modules = path.join(dirInVirtualStore, 'node_modules')lockfileToDepGraph.ts:233 → dir = path.join(modules, pkgName) → resolves to /tmp/pwned (ESCAPES virtual store)storeController.importPackage(depNode.dir, ...) → writes package content to the traversed pathdepPathToFilename() — replaces / with + for the dirInVirtualStore path, but pkgName comes SEPARATELY from dp.parse() and is NOT passed through this functionverifyLockfileResolutions() — validates dependency map keys (aliases) via isValidDependencyAlias(), but never validates the depPath keys themselvesyaml.load(lockfileRawContent) with no schema validation on packages keysimportPackage() — accepts targetDir and passes it directly to cafsStore.importPackage(targetDir, ...) with zero containment checkWhen 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.
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).
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 hijackingCraft a pnpm-lock.yaml:
lockfileVersion: '9.0'
packages:
../../../../../../../tmp/[email protected]:
resolution: {integrity: sha512-<real-package-integrity>}
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.
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 packageLocationAlternatively, validate depPath keys during lockfile parsing to reject any that don't produce valid npm package names via dp.parse().
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.