| Gói / sản phẩm | Phiên bản bị ảnh hưởng | Phiên bản đã vá |
|---|---|---|
| pnpm | ≥ 11.0.0, < 11.11.0< 10.34.5 | 11.11.010.34.5 |
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 , but never validates the depPath keys themselvesBạn đang dùng một trong các sản phẩm này? Quét site để biết có đang lộ phiên bản có lỗ hổng không.
Quét site miễn phíisValidDependencyAlias()yaml.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.