Skip to content

chore(deps): update pnpm to v11.8.0 [security]#2701

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-pnpm-vulnerability
Open

chore(deps): update pnpm to v11.8.0 [security]#2701
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-pnpm-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
pnpm (source) 11.7.011.8.0 age confidence

pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config

GHSA-qrv3-253h-g69c

More information

Details

Summary

pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config.

A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name such as ../../PWNED_CFGDEP. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name.

In local testing against pnpm v11.5.1, this caused pnpm to create a symlink outside the intended config dependency directory:

expected root: /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config
actual path:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP

This works with --ignore-scripts, so it does not rely on lifecycle script execution.

Vulnerable behavior

The vulnerable behavior appears to be that configDependencies keys from the env lockfile are trusted as package names and used in filesystem paths without rejecting traversal components.

The relevant pattern is:

const configModulesDir = path.join(opts.rootDir, 'node_modules/.pnpm-config')

for (const [pkgName, pkg] of Object.entries(normalizedDeps)) {
  const configDepPath = path.join(configModulesDir, pkgName)

  const pkgDirInGlobalVirtualStore = path.join(
    globalVirtualStoreDir,
    relPath,
    'node_modules',
    pkgName
  )

  await symlinkDir(pkgDirInGlobalVirtualStore, configDepPath)
}

If pkgName is attacker-controlled and contains .., then path.join(configModulesDir, pkgName) can resolve outside node_modules/.pnpm-config.

Impact

A malicious project can cause pnpm to create symlinks outside the intended node_modules/.pnpm-config directory during install.

This gives an attacker a filesystem write primitive in the victim project directory, and potentially outside it with deeper traversal payloads, depending on path permissions and platform behavior.

The issue is especially relevant because:

  • The malicious input is committed in pnpm-lock.yaml.
  • The issue is triggered during pnpm install.
  • It works with --ignore-scripts.
  • It occurs in the config dependency installation path, before ordinary dependency installation.
  • The user only needs to install a malicious or compromised repository.
Local proof of concept

The following local-only PoC creates a temporary project, starts a local fake registry on 127.0.0.1, writes a malicious env-lockfile entry, runs pnpm, and checks whether pnpm created a symlink outside node_modules/.pnpm-config.

Command used:

python3 ../pnpm_configdeps_path_traversal_poc.py \
  --pnpm-cmd "node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs" \
  --keep 2>&1 | tee /tmp/pnpm-configdeps-poc.log

Observed output:

[+] Test project:       /tmp/pnpm-cfgdep-poc-sznwgunx/victim
[+] Local registry:     http://127.0.0.1:36545/
[+] Store dir:          /tmp/pnpm-cfgdep-poc-sznwgunx/store
[+] Malicious name:     '../../PWNED_CFGDEP'
[+] Intended cfg root:  /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config
[+] Traversal sink:     /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP
[+] Lockfile written:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/pnpm-lock.yaml
[+] Running: node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs install --ignore-scripts --config.confirmModulesPurge=false --reporter=append-only --store-dir /tmp/pnpm-cfgdep-poc-sznwgunx/store --registry http://127.0.0.1:36545/

pnpm output:

Installing config dependencies...
Installed config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0
Already up to date

Done in 906ms using pnpm v11.5.1

The PoC then detected the escaped symlink:

[+] Traversal sink status: symlink -> ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP

[VULNERABLE] pnpm created/modified a path derived from a lockfile package name outside node_modules/.pnpm-config
            sink = /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP
            readlink = ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP
Malicious lockfile structure

The malicious input is an env-lockfile configDependencies key containing traversal components:

importers:
  .:
    configDependencies:
      legit-config-dep:
        specifier: '1.0.0'
        version: '1.0.0'
      '../../PWNED_CFGDEP':
        specifier: '1.0.0'
        version: '1.0.0'

pnpm accepts the traversal-shaped name and reports it as installed:

Installed config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0
Security boundary violation

The intended config dependency root was:

/tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config

But pnpm created:

/tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP

This demonstrates that a config dependency name from the lockfile can escape the directory where config dependencies should be linked.

Suggested remediation

Validate every configDependencies key loaded from the env lockfile before using it as a package name or path component.

Recommended fixes:

  1. Reject env-lockfile configDependencies names that are not valid npm package names.

  2. Reject names containing absolute paths, . components, .. components, backslashes, or platform-specific path separators.

  3. Use containment-checked path joining before creating symlinks:

    • resolve the final destination path,
    • verify it remains inside node_modules/.pnpm-config,
    • reject if it escapes.
  4. Apply the same validation to config dependency subdependencies and optional dependency names read from the env lockfile.

  5. Intersect env-lockfile configDependencies with the effective pnpm-workspace.yaml configDependencies before installing, so extra lockfile-only entries are rejected.

A safe destination check should enforce behavior equivalent to:

const dest = path.resolve(configModulesDir, pkgName)

if (!dest.startsWith(path.resolve(configModulesDir) + path.sep)) {
  throw new Error(`Invalid config dependency name: ${pkgName}`)
}

Name validation should happen before this check, not instead of it.

Severity

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

References

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


Release Notes

pnpm/pnpm (pnpm)

v11.8.0

Compare Source

Minor Changes
  • c112b61: Added a --dry-run option to pnpm install. It runs a full dependency resolution and reports what an install would change, but writes nothing to disk (no lockfile, no node_modules) and always exits with code 0. This mirrors the preview semantics of npm install --dry-run #​7340.

  • 179ebc4: pnpm run --no-bail now exits with a non-zero exit code when any of the executed scripts fail, while still running every matched script to completion. This makes the exit-code behavior of --no-bail consistent between recursive and non-recursive runs (recursive runs already failed at the end). Previously, a non-recursive pnpm run --no-bail always exited with code 0, even when a script failed #​8013.

  • 0474a9c: Added support for generating Node.js package maps at node_modules/.package-map.json during isolated and hoisted installs. Added the node-experimental-package-map setting to inject the generated map into pnpm-managed Node.js script environments, and the node-package-map-type setting to choose between standard and loose package maps.

  • dcededc: pnpm sbom now marks components reachable only through devDependencies with CycloneDX scope: "excluded" and the cdx:npm:package:development property. The excluded scope documents "component usage for test and other non-runtime purposes", which matches the semantics of a devDependency; the property is the CycloneDX npm-taxonomy marker emitted by @cyclonedx/cyclonedx-npm, so both modern (scope) and existing (property) consumers are covered. Components reachable at runtime (including installed optionalDependencies) omit scope and default to required.

  • 1495cb0: Added per-package SBOM generation with --out and --split flags. Use --out out/%s.cdx.json to write one SBOM per workspace package to individual files, or --split for NDJSON output to stdout. When --filter selects a single package, the SBOM root component now uses that package's metadata. Workspace inter-dependencies (workspace: protocol) and their transitive dependencies are included. Author, repository, and license fall back to the root manifest when the package doesn't define them.

  • 293921a: feat(view): support searching project manifest upward when package name is omitted

    When running pnpm view without a package name, the command now searches
    upward for the nearest project manifest (package.json, package.yaml, or package.json5) and uses its name field.
    If the manifest exists but lacks a name field, an error is thrown.

    This change also replaces the find-up dependency with empathic for
    improved performance and consistency across workspace tools.

Patch Changes
  • 29ab905: Fixed pnpm update overriding the version range policy of a named catalog whose name parses as a version (e.g. catalog:express4-21). The catalog: reference carries no pinning of its own, so the prefix from the catalog entry (such as ~) is now preserved instead of being widened to ^ #​10321.

  • bee4bf4: Security: validate config dependency names and versions from the env lockfile (pnpm-lock.yaml) before using them to build filesystem paths. A committed lockfile with a traversal-shaped configDependencies name (such as ../../PWNED) or version (such as ../../../PWNED) could previously cause pnpm install to create symlinks or write package files outside node_modules/.pnpm-config and the store. Names must now be valid npm package names and versions must be exact semver versions; the same validation is applied to optional subdependencies of config dependencies, and to the legacy workspace-manifest format before any lockfile is written. See GHSA-qrv3-253h-g69c.

  • 96bdd57: Fix link: workspace protocol switching to file: after pnpm rm is run from inside a workspace package whose target workspace dependency has its own dependencies, when injectWorkspacePackages: true is set. Follow-up to #​10575, which fixed the same symptom for workspace packages without dependencies.

  • 302a2f7: No longer warn about using both packageManager and devEngines.packageManager when the two fields pin the same package manager at the same version with the same integrity hash (e.g. both pnpm@11.5.1+sha512.…). Previously the hash was stripped from the legacy packageManager field but not from devEngines.packageManager, so even identical specifications looked like a mismatch #​12028.

    The warning still fires on any genuine divergence, and several cases now state the specific reason instead of a single generic message: a different package manager, a different version, or contradictory integrity hashes for the same version.

  • 3f0fb21: Fixed the progress line showing leftover characters from external processes that write to the terminal between progress updates (e.g. an SSH passphrase prompt would leave a fragment like added 0sa':). The interactive reporter now redraws each frame in place, erasing to the end of the display before reprinting, so any such remnants are cleared #​12350.

  • 564619f: Fixed pnpm approve-builds reporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. after git stash drops the allowBuilds from pnpm-workspace.yaml) is re-added. The revoked packages are now correctly recorded in .modules.yaml so approve-builds can find them. #​12221

  • 3d1fd20: Skip the redundant "target bin directory already contains an exe called node" warning on Windows when the existing node.exe already matches the target (same hard link or identical content) pnpm/pnpm#12203.

  • 1b02b47: Fix macOS Gatekeeper blocking native binaries (.node, .dylib, .so) by removing the com.apple.quarantine extended attribute after importing them from the store.

    When pnpm imports files from its content-addressable store into node_modules, macOS preserves extended attributes, including com.apple.quarantine. If this xattr is present on a store blob (e.g. it was first written under a Gatekeeper-enabled app such as a Git client), it propagates to node_modules, and Gatekeeper blocks the native binary from loading even though pnpm already verified the file's integrity against the lockfile.

    After importing a package, pnpm now strips com.apple.quarantine from its native binaries, matching Homebrew's behaviour of dropping quarantine from verified downloads. The cleanup is macOS-only, runs in a single batched xattr call per package, is restricted to native binaries (other files are untouched), and is non-fatal (it logs a warning on unexpected errors).

    Fixes #​11056

  • 61969fb: Fix pnpm install with optimisticRepeatInstall incorrectly reporting Already up to date when pnpm-lock.yaml changed but project manifests did not. This affected workflows such as checking out or restoring only the lockfile #​12100.

    Also fixes checkDepsStatus to use the correct lockfile path when useGitBranchLockfile is enabled, so the optimistic fast-path and lockfile modification detection work with pnpm-lock.<branch>.yaml files instead of always stat'ing pnpm-lock.yaml. Merge-conflict detection now reads the resolved lockfile name as well, and with mergeGitBranchLockfiles enabled every pnpm-lock.*.yaml is scanned for modifications and conflicts. The git branch is now resolved by reading .git/HEAD directly (no process spawn) and uses the workspace directory rather than process.cwd().

  • 5c12968: Fix recursive updates of transitive dependencies when the update command mixes transitive dependency patterns with direct dependency selectors. For example, pnpm up -r "@&#8203;babel/core" uuid now updates matching transitive @babel/core dependencies even when uuid is a direct dependency selector #​12103.

  • 9d79ba1: Register the pnpm update --no-save flag in the CLI help and option parser.

  • 0474a9c: Fixed pnpm import for Yarn v2 lockfiles when js-yaml v4 is installed.

  • 9e0c375: Fixed pnpm install repeatedly prompting to remove and reinstall node_modules in a workspace package when enableGlobalVirtualStore is enabled. The post-install build step recorded a per-project node_modules/.pnpm virtual store directory in node_modules/.modules.yaml, overwriting the global <storeDir>/links value the install step had written. The next install then detected a virtual-store mismatch (ERR_PNPM_UNEXPECTED_VIRTUAL_STORE). The build step now derives the same global virtual store directory as the install step #​12307.

  • 223d060: Document the --cpu, --os and --libc flags in the output of pnpm install --help. These flags were already supported but were only documented on the website #​12359.

  • e85aea2: Avoid reading README.md from disk when publishing if the publish manifest already provides a readme field. The README is now only read lazily, inside createExportableManifest, when it is actually needed.

  • 3188ae7: Fixed pnpm peers check to accept loose peer dependency ranges such as >=3.16.0 || >=4.0.0- when the installed peer version satisfies the range #​12149.

  • 531f2a3: Fixed pnpm update rewriting a workspace: dependency that points at a local path (e.g. workspace:../packages/foo/dist) into a normalized link: or version-range specifier. Such specifiers are now preserved verbatim when the workspace protocol is preserved #​3902.

  • fe66535: Fixed a lockfile non-convergence bug where an incremental install kept a duplicate transitive dependency that a fresh install would not produce. When a package is reused from the lockfile, its child edges are taken verbatim and bypass the preferred-versions walk, so a transitive dependency could stay pinned to an older version even after a direct dependency resolved to a higher version that satisfies the same range. The resolver now refreshes such a stale pin to the higher direct-dependency version during resolution — so the older version is never resolved or fetched, and the incremental result converges to the fresh one.

  • 6d35338: pnpm install detects changes inside local file dependencies again. The optimistic repeat-install fast path only tracks manifest and lockfile modification times, so edits inside a local dependency's directory (or a repacked local tarball) were reported as "Already up to date". Projects with local file dependencies (file: and bare local path or tarball specifiers, declared directly or through pnpm.overrides) now always run a full install, which refetches those dependencies, matching pnpm v10 behavior #​11795.

  • 4ca9247: Preserve the existing Node.js runtime version prefix when resolving node@runtime:<range> to a concrete version.

  • 30c7590: Create shorter CAFS temporary package directories to leave room for lifecycle scripts that create IPC socket paths under TMPDIR.

  • 13815ad: Reporter output (warnings, progress) for pnpm store and pnpm config subcommands now goes to stderr instead of stdout. This fixes scripts that capture their stdout (e.g. PNPM_STORE=$(pnpm store path), pnpm config list --json | jq) from getting warnings mixed into the result.

  • 1c05876: Avoid relinking unchanged child dependencies and remove stale child links during warm installs.

  • 817f99d: Fixed lockfile churn where a package's transitivePeerDependencies could be dropped (and shift between packages) when the package participates in a dependency cycle. A cycle re-entry resolves against truncated children, so it must not be cached as "pure"; otherwise sibling occurrences of the same package short-circuit and lose transitive peers depending on traversal order #​5108.

  • eba03e0: Fix pnpm install reporting "Already up to date" after a catalog entry in pnpm-workspace.yaml was reverted to a previous version. After an update modified a catalog, the workspace state cache stored the pre-update catalog versions, so reverting the entry back to its original version was not detected as an outdated state #​12418.

  • 3b54d79: pnpm update now keeps lockfile overrides that resolve through a catalog in sync with the catalog. Previously, when an override referenced a catalog (e.g. overrides: { foo: 'catalog:' }) and pnpm update bumped that catalog entry, the lockfile's catalogs advanced while the resolved overrides kept the old version. The resulting lockfile was internally inconsistent, so a later pnpm install --frozen-lockfile failed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.

  • 9d0a300: Fixed pnpm version --recursive so it honors the workspace selection. In recursive mode the version bump now applies to the packages resolved from the workspace filter (selectedProjectsGraph), matching the behavior of pnpm publish --recursive, instead of always bumping every workspace package #​11348.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

Rebasing: Whenever PR is behind base branch, 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.

@renovate

renovate Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: pnpm-lock.yaml
[WARN] "packageManager" and "devEngines.packageManager" specify different versions of pnpm in package.json. "packageManager" will be ignored
[ERROR] This project is configured to use 11.7.0 of pnpm. Your current pnpm is v11.8.0
If you want to bypass this version check, you can set the "pmOnFail" configuration to "warn" or "ignore" (e.g. via --pm-on-fail=ignore). If using "devEngines.packageManager", you can set its "onFail" to "warn" or "ignore"

@apify-service-account

apify-service-account commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

✅ Preview for this PR (commit d2bb8c2) is ready at https://pr-2701.preview.docs.apify.com (see action run).

@renovate renovate Bot force-pushed the renovate/npm-pnpm-vulnerability branch 3 times, most recently from f1df8f8 to 44969ee Compare June 30, 2026 09:15
@renovate renovate Bot force-pushed the renovate/npm-pnpm-vulnerability branch from 44969ee to d2bb8c2 Compare June 30, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant