Skip to content

fix: reject path-traversal config dependency names from the env lockfile#12470

Merged
zkochan merged 12 commits into
mainfrom
fix/config-deps-path-traversal-ghsa
Jun 17, 2026
Merged

fix: reject path-traversal config dependency names from the env lockfile#12470
zkochan merged 12 commits into
mainfrom
fix/config-deps-path-traversal-ghsa

Conversation

@zkochan

@zkochan zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the path-traversal vulnerability reported in GHSA-qrv3-253h-g69c (CWE-22, High).

Config dependency names and versions are read from the committed env lockfile (pnpm-lock.yaml) and the legacy inline-integrity format in pnpm-workspace.yaml. Both become path segments of the directories pnpm creates during install — node_modules/.pnpm-config/<name> and the global virtual store's <name>/<version>/<hash> — but they were used unvalidated. A malicious repository could commit a traversal-shaped name (../../PWNED) or version (../../../PWNED) and make pnpm install create symlinks or write package files outside those roots, triggered on install even with --ignore-scripts.

Fix

Add an offline structural gate, verifyEnvLockfile (verify_env_lockfile in pacquet), that validates every config-dependency and optional-subdependency name (must be a valid npm package name) and version (must be an exact semver version) before any path is built from it. It runs at the install boundary and, through a single writeVerifiedEnvLockfile seam, before the env lockfile is ever persisted — so an invalid entry is rejected with no write side effect. __proto__ names are rejected too (the validation accumulators use null-prototype objects so the key can't slip past Object.keys). The same fix and structure land in pacquet to keep the two stacks in sync.

Tests

  • TS (installing/env-installer) and pacquet (crates/env-installer): regression tests covering traversal-shaped names and versions, __proto__, and the legacy/workspace migration path — asserting rejection, no escaped entry under the project or store, and no pnpm-lock.yaml write on invalid input.

All env-installer tests pass on both stacks; lint, cspell, rustfmt, taplo, clippy, and dylint are clean.

Squashed commit body

Config dependency names and versions are read from the committed env lockfile
(pnpm-lock.yaml) and the legacy inline-integrity format in pnpm-workspace.yaml,
and both become path segments of the directories pnpm creates during install
(node_modules/.pnpm-config/<name> and the global virtual store's
<name>/<version>/<hash>). They were used unvalidated, so a malicious repository
could commit a traversal-shaped name (../../PWNED) or version (../../../PWNED)
and make `pnpm install` create symlinks or write package files outside those
roots — triggered on install, even with --ignore-scripts.

Add verifyEnvLockfile, an offline structural gate that validates every config
dependency and optional-subdependency name (must be a valid npm package name)
and version (must be an exact semver version) before any path is built from it.
It runs at the install boundary and, through a single writeVerifiedEnvLockfile
seam, before the env lockfile is ever persisted, so an invalid entry is rejected
with no write side effect. __proto__ names are rejected too (the validation
accumulators use null-prototype objects so the key can't slip past Object.keys).

The same fix and structure land in pacquet to keep the two stacks in sync.

Fixes GHSA-qrv3-253h-g69c.

Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Hardened config-dependency handling to reject invalid dependency names and require exact semver versions before any filesystem path construction, symlink materialization, or lockfile/workspace migration.
    • Added prototype-key hardening to avoid unsafe object-key behavior during optional subdependency processing and lockfile normalization.
  • Tests

    • Expanded security regression tests to cover traversal-shaped inputs, __proto__ cases, and legacy/workspace migration scenarios—verifying no escaped entries or unintended pnpm-lock.yaml writes occur.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

fix: reject path-traversal config dependency names from the env lockfile
🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Description

• Fixes GHSA-qrv3-253h-g69c (CWE-22, High): config dependency names from pnpm-lock.yaml were used
 directly to build filesystem paths, allowing a malicious lockfile with a traversal-shaped name like
 ../../PWNED to create symlinks outside node_modules/.pnpm-config.
• Adds assertValidConfigDepNames (TS) and assert_valid_config_dep_names (Rust/pacquet) to
 validate every config dep name and optional subdep name as a valid npm package name before any path
 is constructed.
• Adds regression tests in both the TypeScript and Rust env-installer test suites asserting
 traversal-shaped names are rejected and no path escapes the config-dep directory.
• Adds a changeset entry marking @pnpm/installing.env-installer and pnpm as patched with a
 security advisory reference.
Diagram

graph TD
    LF["pnpm-lock.yaml\n(attacker-controlled)"] --> NF["normalizeForInstall /\nnormalize_from_lockfile"]
    NF --> VG["assertValidConfigDepNames /\nassert_valid_config_dep_names"]
    VG -- "invalid name" --> ERR["ERR_PNPM_INVALID_DEPENDENCY_NAME"]
    VG -- "all names valid" --> FS["node_modules/.pnpm-config\n(filesystem writes)"]
    VG --> VAL["assertValidDependencyAliases /\nis_valid_old_npm_package_name"]
    VAL --> VG

    subgraph Legend
      direction LR
      _doc["pnpm-lock.yaml"] ~~~ _fn["Function"] ~~~ _err["Error"] ~~~ _fs[("Filesystem")]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Path canonicalization after construction
  • ➕ Catches traversal even if validation is bypassed by an edge case
  • ➖ More complex to implement correctly across both TS and Rust
  • ➖ Harder to audit; easy to miss a code path
  • ➖ Does not prevent the name from being used in other non-path contexts
2. Validate at lockfile-read layer
  • ➕ Prevents invalid names from ever entering the system
  • ➖ Requires changes in the lockfile parsing crate, a wider blast radius
  • ➖ Would need to handle the legacy inline format separately
  • ➖ Lockfile parsing is a lower-level concern; security policy belongs closer to the install logic

Recommendation: The PR's approach — validating at the chokepoint right after lockfile normalization, before any path is built — is the correct and minimal fix. An alternative would be to sanitize/canonicalize the path after construction (e.g., verify the resolved path is still under .pnpm-config), but that is more error-prone and harder to audit. Another alternative would be to validate at the lockfile-read layer, but that would require changes in more places and could miss the legacy inline format path. The current approach reuses the existing assertValidDependencyAliases / is_valid_old_npm_package_name validators, keeping the fix consistent with how pnpm validates user-supplied dependency names elsewhere.

Files changed (7) +228 / -15

Bug fix (3) +72 / -0
installConfigDeps.tsValidate config dep names before building filesystem paths (TS) +21/-0

Validate config dep names before building filesystem paths (TS)

• Imports 'assertValidDependencyAliases' from '@pnpm/installing.deps-resolver' and calls the new 'assertValidConfigDepNames' guard immediately after 'normalizeForInstall'. The guard validates every top-level config dep name and every optional subdep name as a valid npm package name, throwing 'ERR_PNPM_INVALID_DEPENDENCY_NAME' on any traversal-shaped or otherwise invalid name.

installing/env-installer/src/installConfigDeps.ts

errors.rsAdd 'InvalidDependencyName' error variant to 'ConfigDepError' (Rust) +17/-0

Add 'InvalidDependencyName' error variant to 'ConfigDepError' (Rust)

• Introduces a new 'InvalidDependencyName' variant with 'ERR_PNPM_INVALID_DEPENDENCY_NAME' diagnostic code, mirroring the TypeScript error thrown by 'assertValidDependencyAliases'. Includes a user-facing help message explaining valid npm package name rules.

pacquet/crates/env-installer/src/errors.rs

install_config_deps.rsValidate config dep names before building filesystem paths (Rust/pacquet) +34/-0

Validate config dep names before building filesystem paths (Rust/pacquet)

• Imports 'is_valid_old_npm_package_name' and calls the new 'assert_valid_config_dep_names' function immediately after 'normalize_from_lockfile'. The function iterates all config dep names and their optional subdep names, returning 'ConfigDepError::InvalidDependencyName' for any invalid name.

pacquet/crates/env-installer/src/install_config_deps.rs

Tests (2) +136 / -2
installConfigDeps.tsAdd regression tests for path-traversal config dep and subdep names (TS) +55/-0

Add regression tests for path-traversal config dep and subdep names (TS)

• Adds two new Jest tests: one asserting that a top-level config dep with a traversal-shaped name ('../../PWNED') is rejected before any filesystem write, and one asserting the same for an optional subdep name. Both tests verify no path is created outside 'node_modules/.pnpm-config'.

installing/env-installer/test/installConfigDeps.ts

tests.rsAdd regression tests for path-traversal config dep and subdep names (Rust) +81/-2

Add regression tests for path-traversal config dep and subdep names (Rust)

• Adds two async Tokio tests: 'rejects_config_dep_with_path_traversal_name' and 'rejects_optional_subdep_with_path_traversal_name'. Both construct a tampered env lockfile with a traversal-shaped name, call 'install_config_deps', assert the 'InvalidDependencyName' error variant is returned, and verify no file escaped the expected directory.

pacquet/crates/env-installer/src/tests.rs

Other (2) +20 / -13
config-deps-path-traversal.mdAdd changeset for GHSA-qrv3-253h-g69c security patch +6/-0

Add changeset for GHSA-qrv3-253h-g69c security patch

• Documents the security fix as a patch-level change for '@pnpm/installing.env-installer' and 'pnpm', linking to the GHSA advisory.

.changeset/config-deps-path-traversal.md

Cargo.tomlAdd 'pacquet-resolving-parse-wanted-dependency' dependency to env-installer crate +14/-13

Add 'pacquet-resolving-parse-wanted-dependency' dependency to env-installer crate

• Adds 'pacquet-resolving-parse-wanted-dependency' as a workspace dependency so the Rust env-installer can call 'is_valid_old_npm_package_name' for the new validation gate.

pacquet/crates/env-installer/Cargo.toml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Pacquet writes then validates ✓ Resolved 🐞 Bug ⛨ Security
Description
In pacquet’s legacy manifest (pnpm-workspace.yaml / inline-integrity) migration flow, the code
migrates configDependencies into the env lockfile and can write pnpm-lock.yaml before the newer
exact-semver validation runs. This allows an attacker-controlled invalid/traversal-shaped migrated
version (e.g., extracted from +) to be persisted to pnpm-lock.yaml before the run ultimately fails
with an invalid config dependency version error.
Code

pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs[R35-47]

+    // Validate the manifest-sourced names before any lockfile mutation or
+    // write: migrating the legacy inline format records entries and writes
+    // pnpm-lock.yaml, so an invalid name must be refused first to avoid a
+    // pre-rejection write side effect.
+    for name in config_deps.keys() {
+        if !is_valid_old_npm_package_name(name) {
+            return Err(ConfigDepError::InvalidDependencyName {
+                description: "The configDependencies in pnpm-workspace.yaml".to_string(),
+                name: name.clone(),
+            });
+        }
+    }
+
Evidence
The cited legacy migration branches normalize/migrate configDependencies into lockfile state and, as
part of that migration, write pnpm-lock.yaml as a side effect (e.g., via env_lockfile.write(...) /
migrateConfigDepsToLockfile()) before control reaches the later validation step. In the Rust flow,
resolve_and_install_config_deps() writes the env lockfile prior to calling install_config_deps(),
where version validation occurs via node_semver parsing (assert_valid_config_dep_version), so
invalid versions can be written before being rejected; similarly in the TS flow, installConfigDeps()
runs normalizeForInstall() (which can invoke migrateConfigDepsToLockfile() and write
pnpm-lock.yaml/settings) before assertValidConfigDeps() checks versions, and
parseIntegrity()/parse_integrity() returns the raw version substring without semver validation,
enabling invalid/traversal-shaped versions to slip into the written lockfile prior to failure.

pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs[66-102]
pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs[121-137]
pacquet/crates/env-installer/src/parse_integrity.rs[36-51]
pacquet/crates/env-installer/src/install_config_deps.rs[39-47]
pacquet/crates/env-installer/src/install_config_deps.rs[352-387]
installing/env-installer/src/installConfigDeps.ts[35-46]
installing/env-installer/src/installConfigDeps.ts[182-213]
installing/env-installer/src/migrateConfigDeps.ts[95-107]
installing/env-installer/src/parseIntegrity.ts[24-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Legacy inline-integrity configDependencies are migrated into the env lockfile and pnpm-lock.yaml can be written before config dependency versions are validated as exact semver. Update the migration/normalization paths so any invalid/traversal-shaped version extracted from legacy forms is rejected *before* any lockfile/settings write occurs, ensuring pnpm-lock.yaml is not created/modified on invalid input.
## Issue Context
- In the legacy migration branches, versions are parsed/migrated into lockfile state and a write to pnpm-lock.yaml can occur before the later version validation gate runs.
- `parse_integrity()` / `parseIntegrity()` returns the raw `<version>` substring (from `<version>+<integrity>`) without semver validation.
- In Rust, `env_lockfile.write(...)` can occur before calling `install_config_deps(...)`, while version validation is enforced inside `install_config_deps` via `assert_valid_config_dep_version` / `node_semver` parsing.
- In TS, `installConfigDeps()` validates versions only after `normalizeForInstall()`, but `normalizeForInstall()` may call `migrateConfigDepsToLockfile()` which writes the env lockfile/settings.
## Fix Focus Areas
- pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs[66-138]
- pacquet/crates/env-installer/src/parse_integrity.rs[36-51]
- pacquet/crates/env-installer/src/install_config_deps.rs[39-45]
- pacquet/crates/env-installer/src/install_config_deps.rs[352-387]
- installing/env-installer/src/installConfigDeps.ts[35-45]
- installing/env-installer/src/installConfigDeps.ts[182-213]
- installing/env-installer/src/migrateConfigDeps.ts[35-108]
- installing/env-installer/src/parseIntegrity.ts[24-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unvalidated version path traversal ✓ Resolved 🐞 Bug ⛨ Security
Description
installConfigDeps validates attacker-controlled dependency names but still incorporates
attacker-controlled version strings from pnpm-lock.yaml into filesystem paths (GVS relPath uses
//), so a lockfile with version: "../../../PWNED" can normalize outside the intended store links
root and be written to during importPackage/materialize. The same gap exists in pacquet’s mirror
implementation.
Code

installing/env-installer/src/installConfigDeps.ts[R132-151]

+// Config dependency names are read from the committed env lockfile (and the
+// legacy inline format in pnpm-workspace.yaml), so they are attacker-controlled
+// input. Each name becomes a directory entry under node_modules/.pnpm-config and
+// inside the global virtual store, so a traversal-shaped name like
+// `../../PWNED` would let a malicious repository create symlinks outside the
+// config dependency root during install. Reject anything that is not a plain npm
+// package name before any path is built from it.
+function assertValidConfigDepNames (normalizedDeps: Record<string, NormalizedConfigDep>): void {
+  assertValidDependencyAliases(normalizedDeps, 'The configDependencies in the env lockfile (pnpm-lock.yaml)')
+  for (const [pkgName, pkg] of Object.entries(normalizedDeps)) {
+    if (!pkg.optionalSubdeps?.length) continue
+    // A null-prototype object so an attacker-controlled name of `__proto__`
+    // becomes an enumerable own key the validator rejects, rather than
+    // silently setting the prototype and slipping past `Object.keys`.
+    const subdepsByName: Record<string, true> = Object.create(null)
+    for (const subdep of pkg.optionalSubdeps) {
+      subdepsByName[subdep.name] = true
+    }
+    assertValidDependencyAliases(subdepsByName, `The optionalDependencies of config dependency "${pkgName}" in the env lockfile (pnpm-lock.yaml)`)
+  }
Evidence
The code reads version directly from the attacker-controlled env lockfile and later uses it as a
path segment in the computed global-virtual-store relative path. That relative path is then joined
onto the store links directory and passed to the package materialization/import routines, creating a
filesystem write primitive if version contains traversal components. The newly added validation
gate only checks dependency *names*, not versions, so it doesn’t block this vector.

installing/env-installer/src/installConfigDeps.ts[139-152]
installing/env-installer/src/installConfigDeps.ts[198-227]
installing/env-installer/src/installConfigDeps.ts[60-98]
deps/graph-hasher/src/index.ts[265-286]
installing/env-installer/src/installConfigDeps.ts[316-336]
installing/env-installer/src/resolveOptionalSubdeps.ts[33-42]
pacquet/crates/env-installer/src/install_config_deps.rs[63-97]
pacquet/crates/env-installer/src/install_config_deps.rs[342-372]
pacquet/crates/graph-hasher/src/global_virtual_store_path.rs[125-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The env lockfile (`pnpm-lock.yaml`) is attacker-controlled input. This PR adds validation for config dependency names and optional subdependency names, but it does **not** validate the corresponding `version` strings before they are used to form global-virtual-store relative paths (`<name>/<version>/<hash>`). A malicious `version` containing `../` can therefore still cause `path.join(storeDir/links, relPath, ...)` (or Rust `Path::join`) to normalize outside the intended store links directory and be written to during package materialization.
## Issue Context
- In TypeScript, `version` is read verbatim from `lockfile.importers['.'].configDependencies` and stored into `normalizedDeps`, then passed into `calcGlobalVirtualStorePathWithSubdeps(...)`, whose implementation formats a path string containing `${version}`.
- The resulting path is used as the target directory for `store.importPackage(...)`.
- Pacquet mirrors the same structure: `dep.version` is passed into `calc_global_virtual_store_path_with_subdeps(...)`, and the resulting path is joined onto `store_dir.links()` before materialization.
## Fix Focus Areas
- installing/env-installer/src/installConfigDeps.ts[132-152]
- installing/env-installer/src/installConfigDeps.ts[59-98]
- installing/env-installer/src/installConfigDeps.ts[190-229]
- pacquet/crates/env-installer/src/install_config_deps.rs[342-372]
- pacquet/crates/env-installer/src/install_config_deps.rs[63-97]
### Concrete fix
1. Extend the new chokepoint (`assertValidConfigDepNames` / `assert_valid_config_dep_names`) to also validate:
- each config dep `version`
- each optional subdep `version`
2. Prefer a strict semver parse/validation (since config deps are resolved to concrete versions anyway):
- TS: `semver.valid(version) != null` (or equivalent exact-version predicate used elsewhere in env-installer)
- Rust: use the existing `node-semver` workspace dependency (already present) to parse/validate versions.
3. Add regression tests for malicious traversal-shaped **versions** (both parent config dep and optional subdep) asserting the install is rejected and no escaped path is created.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Verifier crashes on null 🐞 Bug ☼ Reliability
Description
verifyEnvLockfile() destructures { version } from each configDependencies entry without
validating that the entry value is a non-null object, so a crafted pnpm-lock.yaml like
configDependencies: { foo: null } can throw a runtime TypeError during install. This bypasses the
intended controlled PnpmError path and yields an install-time crash (DoS) for malformed lockfiles.
Code

installing/env-installer/src/verifyEnvLockfile.ts[R15-25]

+  const configDeps = envLockfile.importers['.']?.configDependencies
+  assertValidDependencyAliases(configDeps, 'The configDependencies in pnpm-lock.yaml')
+  if (configDeps == null) return
+  for (const [name, { version }] of Object.entries(configDeps)) {
+    assertValidConfigDepVersion(name, version)
+    const optionalDeps = envLockfile.snapshots[`${name}@${version}`]?.optionalDependencies
+    if (optionalDeps == null) continue
+    assertValidDependencyAliases(optionalDeps, `The optionalDependencies of config dependency "${name}" in pnpm-lock.yaml`)
+    for (const [subdepName, subdepVersion] of Object.entries(optionalDeps)) {
+      assertValidConfigDepVersion(subdepName, subdepVersion)
+    }
Evidence
The verifier destructures { version } directly from untrusted lockfile entry values, while the
lockfile reader does not enforce any per-entry schema; the same destructuring pattern exists in
normalizeFromLockfile, so malformed values can crash the process instead of producing a controlled
pnpm error.

installing/env-installer/src/verifyEnvLockfile.ts[14-26]
lockfile/fs/src/envLockfile.ts[27-56]
installing/env-installer/src/installConfigDeps.ts[170-178]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`verifyEnvLockfile()` currently assumes `importers['.'].configDependencies[name]` is an object with a string `version`. If a malicious/garbled env lockfile contains `null` (or other unexpected types), destructuring `for (const [name, { version }] of Object.entries(configDeps))` can throw a `TypeError` before any `PnpmError` is raised, resulting in an uncontrolled install-time crash.
### Issue Context
`readEnvLockfile()` only validates top-level keys are objects; it does not validate the per-dependency entry shapes. Both `verifyEnvLockfile()` and `normalizeFromLockfile()` destructure the same `{ version }` shape.
### Fix Focus Areas
- installing/env-installer/src/verifyEnvLockfile.ts[14-27]
### Suggested fix
1. Before destructuring, validate that `configDeps` is a plain object (or at least `typeof === 'object' && !Array.isArray(...)`) and that each entry value is a non-null object.
2. Validate that `version` is a string before passing it to `assertValidConfigDepVersion`.
3. Similarly validate that `optionalDependencies` is an object and that each optional dep version is a string.
4. When any of these shape checks fail, throw a controlled `PnpmError('ENV_LOCKFILE_CORRUPTED', ...)` (or another appropriate pnpm error) rather than allowing a raw `TypeError` to bubble.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Version not canonicalized 🐞 Bug ≡ Correctness
Description
In assertValidConfigDepVersion/assert_valid_config_dep_version, the validator only checks that
the semver parses, but does not enforce that the input is already in canonical/exact form by
round-tripping back to the same string (an exactness policy enforced elsewhere in the repo). Because
the raw version string is embedded directly into // store paths and tarball URL/path
construction, accepting non-canonical-but-parseable versions can lead to inconsistent on-disk layout
or unexpected install failures despite passing validation.
Code

installing/env-installer/src/assertValidConfigDepVersion.ts[R8-15]

+export function assertValidConfigDepVersion (name: string, version: string): void {
+  if (semver.valid(version) == null) {
+    throw new PnpmError(
+      'INVALID_CONFIG_DEP_VERSION',
+      `The config dependency "${name}" has an invalid version "${version}"`,
+      { hint: 'A config dependency version must be an exact semver version.' }
+    )
+  }
Evidence
The cited validators currently treat “exact” as merely “parseable”: the TypeScript implementation
only checks semver.valid(version) != null (even though semver.valid() can effectively
canonicalize/clean input) and the Rust implementation only checks that version.parse::() succeeds,
without verifying that the parsed value serializes back to the original string. The repo already
demonstrates an explicit exact-version policy elsewhere (e.g., comparing a cleaned version against
the input in pnpm, and pacquet’s exact_version() helper enforcing `parsed.to_string() ==
version`), indicating that non-canonical forms are intentionally rejected in other paths;
additionally, both descriptions note that the raw version is used as a literal path segment in the
// store layout, making non-canonical acceptance directly observable as inconsistent store
paths/behavior.

installing/env-installer/src/assertValidConfigDepVersion.ts[1-16]
config/reader/src/index.ts[807-817]
deps/graph-hasher/src/index.ts[280-286]
pacquet/crates/env-installer/src/install_config_deps.rs[342-385]
pacquet/crates/cli/src/cli_args.rs[652-655]
pacquet/crates/graph-hasher/src/global_virtual_store_path.rs[142-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertValidConfigDepVersion()` / `assert_valid_config_dep_version()` currently accept any semver string that is parseable, but they do not ensure the input is already canonical/exact (i.e., that canonicalization or round-trip stringification yields the same string). We want these validators to enforce an *exact* version string before it is used as a filesystem path segment (and for related tarball URL/path construction), preventing inconsistent store layout and unexpected install behavior.
## Issue Context
- In the TS path, `semver.valid(version)` can return a cleaned canonical version that differs from the input; elsewhere in the repo, exactness is enforced by comparing the cleaned version to the original input and rejecting when they differ.
- In the Rust path, `version.parse::<node_semver::Version>()` only checks parseability; pacquet already has an `exact_version()` helper that enforces `parsed.to_string() == version`, indicating parseable-but-non-exact inputs exist and are intentionally rejected in other code paths.
- The raw `version` is embedded into `<name>/<version>/<hash>` store paths (and related tarball URL/path construction), so accepting non-canonical versions can change on-disk layout even when the semantic version is equivalent.
## Fix Focus Areas
- installing/env-installer/src/assertValidConfigDepVersion.ts[1-16]
- pacquet/crates/env-installer/src/install_config_deps.rs[374-385]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Malformed version error text 🐞 Bug ⚙ Maintainability
Description
The pacquet InvalidConfigDepVersion diagnostic format string includes an extra trailing quote, so
the user-facing error message will end with an unintended ". This is a minor but user-visible
regression in error reporting for invalid config-dependency versions.
Code

pacquet/crates/env-installer/src/errors.rs[66]

+    #[display(r#"The config dependency "{name}" has an invalid version "{version}""#)]
Evidence
The #[display(...)] raw string literal for InvalidConfigDepVersion contains two consecutive
double quotes before the raw-string terminator (..."{version}""#), which means the rendered
message will include an unintended trailing quote.

pacquet/crates/env-installer/src/errors.rs[61-71]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConfigDepError::InvalidConfigDepVersion` has a `#[display(...)]` raw string literal that contains an extra `"` at the end, which renders as a trailing quote in the displayed error message.
### Issue Context
This affects user-facing diagnostics when rejecting invalid config-dependency versions.
### Fix Focus Areas
- pacquet/crates/env-installer/src/errors.rs[66-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. __proto__ validation bypass ✓ Resolved 🐞 Bug ⛨ Security
Description
In assertValidConfigDepNames, optional subdependency names are collected into a plain object
({}) and then validated via assertValidDependencyAliases, which checks Object.keys(). If a
malicious lockfile uses the invalid dependency name __proto__, assigning it into these {} maps
can mutate the object prototype instead of creating an enumerable key, so the new invalid-name
rejection gate can be bypassed for that input.
Code

installing/env-installer/src/installConfigDeps.ts[R139-148]

+function assertValidConfigDepNames (normalizedDeps: Record<string, NormalizedConfigDep>): void {
+  assertValidDependencyAliases(normalizedDeps, 'The configDependencies in the env lockfile (pnpm-lock.yaml)')
+  for (const [pkgName, pkg] of Object.entries(normalizedDeps)) {
+    if (!pkg.optionalSubdeps?.length) continue
+    const subdepsByName: Record<string, true> = {}
+    for (const subdep of pkg.optionalSubdeps) {
+      subdepsByName[subdep.name] = true
+    }
+    assertValidDependencyAliases(subdepsByName, `The optionalDependencies of config dependency "${pkgName}" in the env lockfile (pnpm-lock.yaml)`)
+  }
Evidence
assertValidDependencyAliases validates only enumerated keys (Object.keys(deps)), while
installConfigDeps builds name maps from attacker-controlled lockfile data using plain {}
objects. With the special key __proto__, assignments can affect the object’s prototype rather than
creating an enumerable own-property, meaning the invalid name may not appear in Object.keys() and
thus not be rejected by the new chokepoint validation.

installing/deps-resolver/src/validateDependencyAlias.ts[15-30]
installing/env-installer/src/installConfigDeps.ts[139-148]
installing/env-installer/src/installConfigDeps.ts[60-69]
installing/env-installer/src/installConfigDeps.ts[187-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The TS env-installer hardening validates config-dependency and optional-subdep names via `assertValidDependencyAliases`, which enumerates `Object.keys()`. However, the new code builds intermediary name maps with plain `{}` objects; the special key `__proto__` can mutate the prototype instead of becoming an enumerable key, allowing an invalid dependency name to slip past the validation gate.
### Issue Context
This PR’s intent is to reject *all* non–npm-package names from the env lockfile before any downstream logic depends on them. Using null-prototype objects (or `Map`) for attacker-controlled name maps prevents `__proto__`/prototype special-casing and ensures the validator sees and rejects such keys.
### Fix Focus Areas
- installing/env-installer/src/installConfigDeps.ts[139-148]
- installing/env-installer/src/installConfigDeps.ts[60-71]
- installing/env-installer/src/installConfigDeps.ts[187-224]
### Suggested change
- Replace `{}` with `Object.create(null)` for:
- `subdepsByName`
- `optionalSubdepIds`
- the `deps` accumulator in `normalizeFromLockfile`
- (Alternative) switch these name-indexed objects to `Map<string, ...>` and adapt call sites accordingly.
- Add/extend a regression test covering a `__proto__` config dependency name and/or optional subdep name to ensure it is rejected with `ERR_PNPM_INVALID_DEPENDENCY_NAME`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Pacquet test asserts wrong path ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new pacquet/Jest traversal regression tests assert the wrong filesystem locations by checking
for PWNED_* only in the project root (or its parent) even though traversal from
root/node_modules/.pnpm-config (e.g., ../../PWNED_CFGDEP) would normalize to an escape path
under root/node_modules/. As written, the tests could still pass even if
installConfigDeps/install_config_deps reintroduced a traversal/symlink-creation escape (including
the optional-subdep escape within the store links tree).
Code

pacquet/crates/env-installer/src/tests.rs[R409-412]

+    // No symlink escaped node_modules/.pnpm-config.
+    assert!(!root.path().parent().unwrap().join("PWNED_CFGDEP").exists());
+    assert!(!root.path().join("PWNED_CFGDEP").exists());
+}
Evidence
Both implementations build the config modules directory as /node_modules/.pnpm-config
(config_modules_dir/configModulesDir) and then join an attacker-controlled dependency name
onto it; with a traversal-shaped name like ../../PWNED or ../../PWNED_CFGDEP, Node/Rust path
joining and normalization would resolve the resulting target to an escaped location such as
/node_modules/PWNED (or /node_modules/PWNED_CFGDEP). However, the current test assertions check
different, unrelated locations (e.g., /PWNED and /../../PWNED, or /… and /…), so they do not
actually verify that the constructed symlink/entry target was not created at the real escape
destination; similarly, the optional-subdep case can escape within the store links tree, which the
current checks do not directly cover.

pacquet/crates/env-installer/src/install_config_deps.rs[39-66]
pacquet/crates/env-installer/src/tests.rs[373-412]
installing/env-installer/src/installConfigDeps.ts[40-72]
installing/env-installer/test/installConfigDeps.ts[86-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The traversal regression tests for config dependencies (and optional subdeps) are asserting the wrong target paths for the expected escape. Because `installConfigDeps`/`install_config_deps` constructs `configModulesDir = <rootDir>/node_modules/.pnpm-config` and then joins the (attacker-controlled) package name onto it, a name like `../../PWNED` or `../../PWNED_CFGDEP` would normalize to an escaped location under `<rootDir>/node_modules/`, not under `<rootDir>/` (or the root’s parent), so the tests can pass even if a traversal write/symlink escape is reintroduced.
## Issue Context
These tests are intended to prevent regressions of GHSA-qrv3-253h-g69c in pacquet, but the current assertions don’t validate the actual normalized escape destinations (including the optional-subdep case that can escape within the store links tree under the GVS leaf).
## Fix Focus Areas
- pacquet/crates/env-installer/src/tests.rs[373-451]
- installing/env-installer/test/installConfigDeps.ts[86-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread pacquet/crates/env-installer/src/tests.rs Outdated
@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.91837% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.13%. Comparing base (83f06a6) to head (1b9a673).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...et/crates/env-installer/src/verify_env_lockfile.rs 95.55% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12470   +/-   ##
=======================================
  Coverage   88.12%   88.13%           
=======================================
  Files         311      312    +1     
  Lines       41924    41970   +46     
=======================================
+ Hits        36947    36991   +44     
- Misses       4977     4979    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread installing/env-installer/src/installConfigDeps.ts Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 780598a

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Security fix that validates configDependencies and optional subdependency names and versions from pnpm-lock.yaml against valid npm package name rules and exact semver before constructing any filesystem paths. Both the TypeScript (pnpm) and Rust (pacquet) env-installer implementations are patched in parallel, with null-prototype object hardening and rejection of traversal-shaped and __proto__ keys. Validation gates are added at both the lockfile and manifest entry points, preventing side effects from partial processing.

Changes

TypeScript (pnpm) env-installer — validation gates and null-prototype hardening

Layer / File(s) Summary
Semver validation helper
installing/env-installer/src/assertValidConfigDepVersion.ts
New assertValidConfigDepVersion helper validates exact semver versions using semver.valid and throws INVALID_CONFIG_DEP_VERSION errors for invalid inputs. Imported and used by both installConfigDeps and migrateConfigDeps to reject traversal-shaped and otherwise invalid versions.
Validation gates in installConfigDeps
installing/env-installer/src/installConfigDeps.ts
Imports assertValidDependencyAliases and assertValidConfigDepVersion, adds assertValidRawConfigDepNames to validate raw dependency names pre-normalization, and assertValidConfigDeps to validate normalized config dependency and optional subdependency names/versions post-normalization. Both gates reject traversal-shaped and __proto__ keys before any filesystem path construction.
Null-prototype hardening
installing/env-installer/src/installConfigDeps.ts, installing/env-installer/src/migrateConfigDeps.ts
Switches optionalSubdepIds, the normalizeFromLockfile deps accumulator, and migrateConfigDeps accumulators (cleanSpecifiers, normalizedDeps) from plain objects to null-prototype objects using Object.create(null), preventing prototype pollution during attacker-controlled key insertion and validation.
Jest tests
installing/env-installer/test/installConfigDeps.ts
Adds containsEntryNamed recursive filesystem helper and test cases verifying installConfigDeps rejects path-traversal-shaped and __proto__ names for both configDependencies and optionalDependencies, rejects traversal-shaped versions, and rejects invalid manifest-sourced names before lockfile writes. All assert no malicious entries are created under project root or store.
Security changeset
.changeset/config-deps-path-traversal.md
Patch-level changeset documenting the fix for @pnpm/installing.env-installer and pnpm, describing validation of configDependencies, optional subdependency names and versions, and legacy workspace-manifest formats before lockfile writes.

Rust (pacquet) env-installer — validation at lockfile and manifest entry points

Layer / File(s) Summary
Error variant and crate dependency
pacquet/crates/env-installer/src/errors.rs, pacquet/crates/env-installer/Cargo.toml
Adds ConfigDepError::InvalidDependencyName { description, name } variant with ERR_PNPM_INVALID_DEPENDENCY_NAME diagnostics and help text describing valid npm package name rules; updates Cargo.toml to include pacquet-resolving-parse-wanted-dependency workspace dependency supplying is_valid_old_npm_package_name validator.
Lockfile-sourced name and version validation
pacquet/crates/env-installer/src/install_config_deps.rs
Imports is_valid_old_npm_package_name, calls assert_valid_config_deps early in install_config_deps before any filesystem path construction, and implements the helper to iterate every config dependency name and each optional subdependency name/version, rejecting traversal shapes and __proto__ with InvalidDependencyName or InvalidConfigDepVersion on the first invalid entry.
Manifest-sourced name and version validation
pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs
Imports is_valid_old_npm_package_name and validates all config_deps names before any lockfile mutations or writes; also calls assert_valid_config_dep_version during migration of legacy inline config dependency formats before recording lockfile entries. Returns InvalidDependencyName with manifest context on first invalid entry, preventing side effects from partial processing.
Rust regression tests
pacquet/crates/env-installer/src/tests.rs
Adds contains_entry_named recursive directory helper and regression tests asserting rejection of traversal-shaped config dependency names, traversal-shaped optional subdependency names, traversal-shaped versions for both config and optional subdeps, __proto__ config dependency names, and invalid manifest-sourced names before lockfile writing. All verify no malicious entries are created under project root or store links tree.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • pnpm/pnpm#11725: Introduces optionalSubdepIds and optional subdependency filtering logic in installConfigDeps.ts that this PR now hardens with null-prototype objects and validates for traversal/__proto__ names.
  • pnpm/pnpm#12285: Main PR's installConfigDeps now relies on assertValidDependencyAliases to validate config dependency names before path construction, which is directly aligned with the retrieved PR's changes that harden dependency alias validation to reject path-traversal segments used in symlink/path creation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly matches the main change: it documents validation of config dependency names from env lockfiles to block path-traversal attacks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/config-deps-path-traversal-ghsa

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

❤️ Share

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

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/pnpm/pnpm/issues/comments/4728793192","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/pnpm/pnpm/pull/12470?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Path: .coderabbit.yaml\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro Plus\n> \n> **Run ID**: `b93cf90d-c605-41ae-9850-f79b66e6a8b6`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between c1f9cdfe95c31c1d54a4fbe743fe9d78788ec237 and 3098e418ba9eb14bcff2d33ee4e465b9c135d154.\n> \n> </details>\n> \n> <details>\n> <summary>⛔ Files ignored due to path filters (1)</summary>\n> \n> * `Cargo.lock` is excluded by `!**/*.lock`, `!Cargo.lock`\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (8)</summary>\n> \n> * `.changeset/config-deps-path-traversal.md`\n> * `installing/env-installer/src/installConfigDeps.ts`\n> * `installing/env-installer/src/migrateConfigDeps.ts`\n> * `installing/env-installer/test/installConfigDeps.ts`\n> * `pacquet/crates/env-installer/Cargo.toml`\n> * `pacquet/crates/env-installer/src/errors.rs`\n> * `pacquet/crates/env-installer/src/install_config_deps.rs`\n> * `pacquet/crates/env-installer/src/tests.rs`\n> \n> </details>\n> \n> ```ascii\n>  _____________________________________________________\n> < Bugs were harmed in the making of this code review. >\n>  -----------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `fix/config-deps-path-traversal-ghsa`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=pnpm/pnpm&utm_content=12470)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

Comment thread installing/env-installer/src/installConfigDeps.ts Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3098e41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@installing/env-installer/src/installConfigDeps.ts`:
- Around line 38-39: Move the assertValidConfigDepNames validation to run before
normalizeForInstall to prevent malicious configDependencies from triggering file
writes. Call assertValidConfigDepNames on the configDepsOrLockfile input before
passing it to normalizeForInstall, ensuring invalid names are rejected before
any side effects like pnpm-lock.yaml writes can occur.

In `@installing/env-installer/test/installConfigDeps.ts`:
- Around line 191-213: The test function `an optional subdep named __proto__ in
the env lockfile is rejected` verifies that installConfigDeps rejects the
invalid name but does not assert that no `__proto__` artifacts were actually
created in the project or store directories. Add assertions after the expect
rejects call to verify that no `__proto__` entry exists under the project root
directory (using rootDir) and store directory (using storeDir), similar to what
other security regression tests do, to ensure the rejection prevented any side
effects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b93cf90d-c605-41ae-9850-f79b66e6a8b6

📥 Commits

Reviewing files that changed from the base of the PR and between c1f9cdf and 3098e41.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (8)
  • .changeset/config-deps-path-traversal.md
  • installing/env-installer/src/installConfigDeps.ts
  • installing/env-installer/src/migrateConfigDeps.ts
  • installing/env-installer/test/installConfigDeps.ts
  • pacquet/crates/env-installer/Cargo.toml
  • pacquet/crates/env-installer/src/errors.rs
  • pacquet/crates/env-installer/src/install_config_deps.rs
  • pacquet/crates/env-installer/src/tests.rs

Comment thread installing/env-installer/src/installConfigDeps.ts Outdated
Comment thread installing/env-installer/test/installConfigDeps.ts
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.6±0.15ms   568.5 KB/sec    1.00      7.6±0.22ms   568.1 KB/sec

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 041bbfa

Comment thread pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 17bbb97

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.229 ± 0.286 3.924 4.822 1.93 ± 0.18
pacquet@main 4.143 ± 0.181 3.950 4.520 1.89 ± 0.14
pnpr@HEAD 2.283 ± 0.139 2.071 2.468 1.04 ± 0.09
pnpr@main 2.187 ± 0.132 2.006 2.362 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.228610667360001,
      "stddev": 0.2863907075277005,
      "median": 4.138103853760001,
      "user": 3.8349984199999994,
      "system": 3.4848826600000002,
      "min": 3.92397150226,
      "max": 4.8223811882600005,
      "times": [
        4.8223811882600005,
        4.56691352226,
        4.00394932226,
        4.269960715260001,
        4.14792561526,
        4.02638813526,
        4.12828209226,
        4.01533750026,
        3.92397150226,
        4.38099708026
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.143054784259999,
      "stddev": 0.181230035079206,
      "median": 4.08948322376,
      "user": 3.7846206199999997,
      "system": 3.47733826,
      "min": 3.94983425626,
      "max": 4.52038788026,
      "times": [
        4.09104504226,
        4.31905707826,
        3.96888657026,
        4.086230647260001,
        3.98869067026,
        4.52038788026,
        4.08792140526,
        4.28989544426,
        4.12859884826,
        3.94983425626
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.28326037276,
      "stddev": 0.13855306353258703,
      "median": 2.29467456276,
      "user": 2.64072392,
      "system": 2.97867006,
      "min": 2.07149634526,
      "max": 2.46813260326,
      "times": [
        2.14806162026,
        2.38910643926,
        2.42957223226,
        2.19288819826,
        2.39107903426,
        2.1529181292599997,
        2.34861853126,
        2.24073059426,
        2.46813260326,
        2.07149634526
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.18732853586,
      "stddev": 0.13177144818302666,
      "median": 2.20284712476,
      "user": 2.61804482,
      "system": 2.99213846,
      "min": 2.0062820492599998,
      "max": 2.36182075926,
      "times": [
        2.34865212826,
        2.11682741326,
        2.29857104026,
        2.1953509482599998,
        2.36182075926,
        2.2559607962599997,
        2.05983042726,
        2.21034330126,
        2.0062820492599998,
        2.01964649526
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 661.2 ± 17.3 639.9 687.0 1.00
pacquet@main 691.4 ± 105.7 628.8 986.3 1.05 ± 0.16
pnpr@HEAD 714.1 ± 53.5 662.4 853.6 1.08 ± 0.09
pnpr@main 723.3 ± 59.9 681.2 886.4 1.09 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6612121240600001,
      "stddev": 0.01725950877371257,
      "median": 0.6629280507600002,
      "user": 0.36486922,
      "system": 1.3750407799999997,
      "min": 0.6398598962600001,
      "max": 0.6870250762600001,
      "times": [
        0.6870250762600001,
        0.63995411026,
        0.6711116882600001,
        0.6674251592600001,
        0.6584309422600001,
        0.6524004482600001,
        0.6801791982600001,
        0.6737226112600001,
        0.6398598962600001,
        0.64201211026
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.69139275366,
      "stddev": 0.1056871528248886,
      "median": 0.6666094547600001,
      "user": 0.39089202,
      "system": 1.34902008,
      "min": 0.6287913982600001,
      "max": 0.9862641492600002,
      "times": [
        0.6823940262600001,
        0.6287913982600001,
        0.68413404726,
        0.6513099792600001,
        0.6675084172600001,
        0.6332086502600001,
        0.6657104922600001,
        0.67943887426,
        0.6351675022600001,
        0.9862641492600002
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7140832400600001,
      "stddev": 0.05349075565307612,
      "median": 0.69799576626,
      "user": 0.39021081999999996,
      "system": 1.38370878,
      "min": 0.6624304392600001,
      "max": 0.8535951352600001,
      "times": [
        0.6856582702600001,
        0.7378120632600002,
        0.69729495626,
        0.7258022852600001,
        0.6986965762600001,
        0.70470203626,
        0.6624304392600001,
        0.6938612112600001,
        0.8535951352600001,
        0.68097942726
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7233416236600002,
      "stddev": 0.059912203310515166,
      "median": 0.7103703112600002,
      "user": 0.39558232000000004,
      "system": 1.37935568,
      "min": 0.6812326422600001,
      "max": 0.8864348052600001,
      "times": [
        0.7198357812600001,
        0.6812326422600001,
        0.7036810322600001,
        0.7282778762600001,
        0.6892391822600001,
        0.7170595902600001,
        0.6923952792600001,
        0.8864348052600001,
        0.6869457912600001,
        0.7283142562600001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.371 ± 0.041 4.306 4.440 1.99 ± 0.13
pacquet@main 4.366 ± 0.067 4.234 4.485 1.99 ± 0.13
pnpr@HEAD 2.295 ± 0.129 2.110 2.529 1.05 ± 0.09
pnpr@main 2.195 ± 0.144 2.028 2.490 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.370832694000001,
      "stddev": 0.04104188196477578,
      "median": 4.376998839500001,
      "user": 3.7765408199999997,
      "system": 3.4388117,
      "min": 4.3064371085,
      "max": 4.4397487975,
      "times": [
        4.3720389705,
        4.3064371085,
        4.4161802015000005,
        4.383919517500001,
        4.3932918785,
        4.3819587085,
        4.3466012555,
        4.4397487975,
        4.3417238595,
        4.3264266425
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.3661489872,
      "stddev": 0.06663233553064815,
      "median": 4.370324522000001,
      "user": 3.77683442,
      "system": 3.4449490999999997,
      "min": 4.2339497295,
      "max": 4.4845420315,
      "times": [
        4.4845420315,
        4.3427265065,
        4.3389130475,
        4.3593278045,
        4.2339497295,
        4.3813212395,
        4.3903386685,
        4.3176657305,
        4.4254536155,
        4.3872514985
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.2950596298,
      "stddev": 0.1291156765472127,
      "median": 2.2594385595,
      "user": 2.49251562,
      "system": 2.9430559,
      "min": 2.1098411305,
      "max": 2.5286656554999998,
      "times": [
        2.4147670805,
        2.3704174795,
        2.2206992194999997,
        2.5286656554999998,
        2.2260347365,
        2.2928423825,
        2.1098411305,
        2.3940312135,
        2.1675991294999997,
        2.2256982704999997
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1952805767999997,
      "stddev": 0.14434595666662306,
      "median": 2.1539372654999998,
      "user": 2.4828083199999997,
      "system": 2.9477801999999995,
      "min": 2.0280975925,
      "max": 2.4895537365,
      "times": [
        2.1510380115,
        2.1309630815,
        2.1759764644999997,
        2.4186617125,
        2.4895537365,
        2.1568365194999997,
        2.0900666264999996,
        2.1340732565,
        2.0280975925,
        2.1775387664999997
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.360 ± 0.015 1.340 1.386 2.05 ± 0.05
pacquet@main 1.391 ± 0.026 1.355 1.438 2.10 ± 0.06
pnpr@HEAD 0.681 ± 0.067 0.649 0.870 1.03 ± 0.10
pnpr@main 0.664 ± 0.016 0.649 0.707 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3596377136400002,
      "stddev": 0.015458425959476274,
      "median": 1.3535928077400001,
      "user": 1.34660292,
      "system": 1.73065478,
      "min": 1.34018699424,
      "max": 1.3861097122400001,
      "times": [
        1.34986965624,
        1.35447972024,
        1.3861097122400001,
        1.36852666824,
        1.35270589524,
        1.34018699424,
        1.35607298824,
        1.38558768324,
        1.35061889024,
        1.3522189282400001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3913680320399997,
      "stddev": 0.02555071729683921,
      "median": 1.38581741524,
      "user": 1.3844105199999999,
      "system": 1.7468261799999998,
      "min": 1.3549965722400001,
      "max": 1.43840573024,
      "times": [
        1.37537698824,
        1.43840573024,
        1.38215288324,
        1.42886113224,
        1.3970733152400001,
        1.38948194724,
        1.3970067292400001,
        1.37460620624,
        1.3549965722400001,
        1.37571881624
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6814755120400001,
      "stddev": 0.06657954037249862,
      "median": 0.6611613457400001,
      "user": 0.33504771999999994,
      "system": 1.31193668,
      "min": 0.6486177482400001,
      "max": 0.87005536624,
      "times": [
        0.65403764024,
        0.66071894024,
        0.6580829782400001,
        0.87005536624,
        0.66160375124,
        0.6486177482400001,
        0.67162921624,
        0.65794911424,
        0.66571730224,
        0.66634306324
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6641023532400001,
      "stddev": 0.016087664186452928,
      "median": 0.6588523417400001,
      "user": 0.35150832000000004,
      "system": 1.30250598,
      "min": 0.64860921124,
      "max": 0.70665944424,
      "times": [
        0.66992972324,
        0.6660362952400001,
        0.70665944424,
        0.65817556824,
        0.6626604392400001,
        0.65899734524,
        0.65415110424,
        0.6570970632400001,
        0.65870733824,
        0.64860921124
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.995 ± 0.028 2.954 3.060 4.52 ± 0.11
pacquet@main 3.020 ± 0.028 2.977 3.054 4.56 ± 0.11
pnpr@HEAD 0.662 ± 0.015 0.647 0.703 1.00
pnpr@main 0.694 ± 0.085 0.642 0.932 1.05 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.99494486966,
      "stddev": 0.02799022708875373,
      "median": 2.9891137848600002,
      "user": 1.7593895400000001,
      "system": 1.9617115600000001,
      "min": 2.95430110386,
      "max": 3.0597093058600002,
      "times": [
        2.98224618386,
        2.99953274686,
        3.00161577886,
        2.98065081786,
        2.95430110386,
        3.01487793086,
        2.9930377628600002,
        3.0597093058600002,
        2.9851898068600002,
        2.97828725886
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0201101489599997,
      "stddev": 0.027701823062846683,
      "median": 3.02676119636,
      "user": 1.7826252400000002,
      "system": 1.98816526,
      "min": 2.97679283786,
      "max": 3.05356759386,
      "times": [
        3.03268801786,
        3.00213411286,
        3.04556837086,
        2.9787482238600003,
        3.05356759386,
        3.02821424686,
        3.02530814586,
        3.04926717186,
        3.00881276786,
        2.97679283786
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6621774786600001,
      "stddev": 0.015490088062718375,
      "median": 0.6584685783600001,
      "user": 0.33137663999999994,
      "system": 1.3100333599999998,
      "min": 0.6467081918600001,
      "max": 0.7032496138600001,
      "times": [
        0.7032496138600001,
        0.6571726258600001,
        0.6639399118600001,
        0.6513951208600001,
        0.66589723586,
        0.6608763888600001,
        0.6595929968600001,
        0.6467081918600001,
        0.6555985408600001,
        0.65734415986
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6941361933600001,
      "stddev": 0.08497046842429093,
      "median": 0.6687605893600002,
      "user": 0.35596514,
      "system": 1.30126686,
      "min": 0.6418625258600001,
      "max": 0.9324279818600001,
      "times": [
        0.66616564186,
        0.6700336188600001,
        0.6762026468600001,
        0.6602233958600001,
        0.67049574586,
        0.6674875598600001,
        0.6575626978600001,
        0.6418625258600001,
        0.6989001188600001,
        0.9324279818600001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12470
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,370.83 ms
(+3.61%)Baseline: 4,218.39 ms
5,062.06 ms
(86.34%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,994.94 ms
(-0.60%)Baseline: 3,013.03 ms
3,615.63 ms
(82.83%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,359.64 ms
(+2.14%)Baseline: 1,331.12 ms
1,597.35 ms
(85.12%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,228.61 ms
(+0.86%)Baseline: 4,192.59 ms
5,031.11 ms
(84.05%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
661.21 ms
(+7.18%)Baseline: 616.90 ms
740.29 ms
(89.32%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12470
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,295.06 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
662.18 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
681.48 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,283.26 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
714.08 ms
🐰 View full continuous benchmarking report in Bencher

Comment thread pacquet/crates/env-installer/src/errors.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2668731

@zkochan zkochan force-pushed the fix/config-deps-path-traversal-ghsa branch from 2668731 to 06054ce Compare June 17, 2026 12:20
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 06054ce

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5b9b9f8

@zkochan

zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai configuration

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Configuration used: Path: .coderabbit.yaml

CodeRabbit configuration
language: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
focus: casual
reviews:
  profile: chill
  request_changes_workflow: false
  high_level_summary: true
  high_level_summary_instructions: ''
  high_level_summary_placeholder: '`@coderabbitai` summary'
  high_level_summary_in_walkthrough: false
  auto_title_placeholder: '`@coderabbitai`'
  auto_title_instructions: ''
  review_status: true
  review_details: false
  commit_status: true
  fail_commit_status: false
  collapse_walkthrough: true
  changed_files_summary: true
  sequence_diagrams: true
  estimate_code_review_effort: true
  assess_linked_issues: true
  related_issues: true
  related_prs: true
  suggested_labels: true
  labeling_instructions:
    - label: 'product: pnpm'
      instructions: >-
        Apply when changes affect the TypeScript pnpm CLI (any code outside
        pacquet/ and pnpr/).
    - label: 'product: pacquet'
      instructions: >-
        Apply when changes affect pacquet, the Rust port of the pnpm CLI (the
        pacquet/ directory).
    - label: 'product: pnpr'
      instructions: >-
        Apply when changes affect pnpr, the pnpm registry server (the pnpr/
        directory and pacquet/crates/pnpr-* crates).
  auto_apply_labels: true
  suggested_reviewers: true
  auto_assign_reviewers: false
  suggested_reviewers_instructions: []
  in_progress_fortune: true
  poem: false
  enable_prompt_for_ai_agents: true
  path_filters:
    - '!pnpm-lock.yaml'
    - '!Cargo.lock'
    - '!**/dist/**'
    - '!**/__fixtures__/**'
    - '!**/*.snap'
    - '!**/CHANGELOG.md'
  path_instructions:
    - path: '**/*'
      instructions: >
        Apply the "AI Review Guidance" section in AGENTS.md. Security
        vulnerabilities are the first review priority; performance regressions
        in pnpm, pacquet, and pnpr are the second priority. Surface only issues
        tied to changed code, and explain the exploit path, impact, or hot path
        affected.
  abort_on_close: true
  disable_cache: false
  slop_detection:
    enabled: true
  auto_review:
    enabled: true
    description_keyword: ''
    auto_incremental_review: true
    auto_pause_after_reviewed_commits: 5
    ignore_title_keywords: []
    labels: []
    drafts: false
    base_branches: []
    ignore_usernames: []
  finishing_touches:
    docstrings:
      enabled: true
    unit_tests:
      enabled: true
    simplify:
      enabled: false
    custom: []
  pre_merge_checks:
    override_requested_reviewers_only: false
    docstrings:
      mode: warning
      threshold: 80
    title:
      mode: warning
      requirements: ''
    description:
      mode: warning
    issue_assessment:
      mode: warning
    custom_checks: []
  tools:
    ast-grep:
      rule_dirs: []
      util_dirs: []
      essential_rules: true
      packages: []
    shellcheck:
      enabled: true
    ruff:
      enabled: true
    markdownlint:
      enabled: true
    github-checks:
      enabled: true
      timeout_ms: 90000
    languagetool:
      enabled: true
      enabled_rules: []
      disabled_rules: []
      enabled_categories: []
      disabled_categories: []
      enabled_only: false
      level: default
    biome:
      enabled: true
    hadolint:
      enabled: true
    swiftlint:
      enabled: true
    phpstan:
      enabled: true
      level: default
    phpmd:
      enabled: true
    phpcs:
      enabled: true
    golangci-lint:
      enabled: true
    yamllint:
      enabled: true
    gitleaks:
      enabled: true
    trufflehog:
      enabled: true
    checkov:
      enabled: true
    tflint:
      enabled: true
    detekt:
      enabled: true
    eslint:
      enabled: true
    flake8:
      enabled: true
    fbinfer:
      enabled: false
      enable_java: false
    fortitudeLint:
      enabled: true
    rubocop:
      enabled: true
    buf:
      enabled: true
    regal:
      enabled: true
    actionlint:
      enabled: true
    zizmor:
      enabled: true
    pmd:
      enabled: true
    clang:
      enabled: true
    cppcheck:
      enabled: true
    opengrep:
      enabled: true
    semgrep:
      enabled: true
    circleci:
      enabled: true
    clippy:
      enabled: true
    sqlfluff:
      enabled: true
    trivy:
      enabled: true
    prismaLint:
      enabled: true
    pylint:
      enabled: true
    oxc:
      enabled: true
    shopifyThemeCheck:
      enabled: true
    luacheck:
      enabled: true
    brakeman:
      enabled: true
    dotenvLint:
      enabled: true
    htmlhint:
      enabled: true
    stylelint:
      enabled: true
    checkmake:
      enabled: true
    osvScanner:
      enabled: true
    oasdiff:
      enabled: true
    presidio:
      enabled: false
    blinter:
      enabled: true
    smartyLint:
      enabled: true
    emberTemplateLint:
      enabled: true
    skillspector:
      enabled: true
    psscriptanalyzer:
      enabled: true
chat:
  art: true
  allow_non_org_members: true
  auto_reply: true
  integrations:
    jira:
      usage: auto
    linear:
      usage: auto
knowledge_base:
  opt_out: false
  web_search:
    enabled: true
  code_guidelines:
    enabled: true
    filePatterns: []
  learnings:
    scope: local
    approval_delay: 0
  issues:
    scope: auto
  jira:
    usage: auto
    project_keys: []
  linear:
    usage: auto
    team_keys: []
  pull_requests:
    scope: auto
  mcp:
    usage: auto
    disabled_servers: []
  automatic_repository_linking: false
  linked_repositories: []
code_generation:
  docstrings:
    path_instructions: []
  unit_tests:
    path_instructions: []
issue_enrichment:
  auto_enrich:
    enabled: false
  planning:
    enabled: true
    auto_planning:
      enabled: true
      labels: []
  labeling:
    labeling_instructions:
      - label: 'type: bug'
        instructions: Apply when the issue reports incorrect or unexpected behavior.
      - label: 'type: feature'
        instructions: Apply when the issue requests new functionality or an enhancement.
      - label: 'type: question'
        instructions: >-
          Apply when the issue is a usage question rather than a bug or feature
          request.
      - label: 'area: lockfile'
        instructions: >-
          Apply when the issue concerns pnpm-lock.yaml parsing, format, or
          handling.
      - label: 'area: monorepo'
        instructions: Apply when the issue concerns the pnpm workspace feature.
      - label: 'area: lifecycle-scripts'
        instructions: Apply when the issue concerns running package lifecycle/build scripts.
      - label: 'area: peers'
        instructions: Apply when the issue concerns peer dependency resolution.
      - label: 'area: catalogs'
        instructions: Apply when the issue concerns the catalogs feature.
      - label: 'area: patching'
        instructions: >-
          Apply when the issue concerns patching packages (pnpm patch /
          patchedDependencies).
      - label: 'area: resolution'
        instructions: Apply when the issue concerns dependency resolution.
      - label: 'area: supply chain security'
        instructions: >-
          Apply when the issue concerns minimumReleaseAge, blockExoticSubdeps,
          build-script safety, or trust policies.
      - label: 'area: config dependencies'
        instructions: Apply when the issue concerns configDependencies.
    auto_apply_labels: true

@zkochan

zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 17, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c94ecfc

@zkochan

zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

/improve

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c94ecfc

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6664b98

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ce6d981

zkochan added 11 commits June 17, 2026 23:18
Config dependency names (and their optional subdependency names) read from
the committed env lockfile (pnpm-lock.yaml) were used directly to build
filesystem paths under node_modules/.pnpm-config. A malicious repository
could commit a traversal-shaped name such as `../../PWNED` and make
`pnpm install` create symlinks outside that directory, even with
`--ignore-scripts`.

Validate every config dependency and optional subdependency name as a valid
npm package name before any path is built from it, reusing the existing
assertValidDependencyAliases / is_valid_old_npm_package_name check. The same
fix is applied to both the TypeScript CLI and pacquet.

Fixes GHSA-qrv3-253h-g69c.
…tions

The earlier assertions probed guessed locations rather than where the symlink
would actually land. A traversal-shaped config dep name joined onto
<rootDir>/node_modules/.pnpm-config normalizes to <rootDir>/PWNED, and an
optional subdep escapes into the store links tree. Replace the point checks
with a recursive search (that does not follow symlinks) over both the project
and the store directories, so a reintroduced escape to any of those locations
is caught.
…cumulators

The name-validation gate collected config dependency and optional subdependency
names into plain `{}` objects before validating their `Object.keys`. A name of
`__proto__` set the accumulator's prototype instead of becoming an enumerable
own key, so the validator never saw it and the invalid name slipped past the
gate — the optional subdep then reached the install/fetch step.

Build those accumulators (and the `normalizeFromLockfile` / migration result
maps) with `Object.create(null)`, so a `__proto__` name lands as an own key the
validator rejects with ERR_PNPM_INVALID_DEPENDENCY_NAME. Added regression tests
on both stacks; pacquet's string-keyed maps have no prototype semantics and
already rejected it, so the Rust test just pins that parity.
… lockfile

On the legacy manifest-migration path, names were normalized — which writes
pnpm-lock.yaml and workspace settings to disk — before the validation gate ran,
so a malicious name could trigger those writes and only then be rejected.

Validate the raw manifest/lockfile config dependency names up front, before any
normalization or lockfile write, so an invalid name is refused with no side
effects. Applied to both stacks (TS `installConfigDeps`, pacquet
`resolve_and_install_config_deps`), with regression tests asserting no
pnpm-lock.yaml is written. Also added the missing no-artifact assertions to the
optional-subdep `__proto__` test for consistency.
…ckfile

A config dependency's `version` is also a global-virtual-store path segment
(`<name>/<version>/<hash>`), so a committed lockfile with a traversal-shaped
version such as `../../../PWNED` could escape the store links root and write
package files outside it during install — the name-only validation didn't cover
this.

Validate every config dependency and optional subdependency version as an exact
semver version at the same chokepoint that validates names, before any path is
built. Applied to both stacks (TS `installConfigDeps`, pacquet
`install_config_deps`) with regression tests for traversal-shaped parent and
subdep versions; verified each fails without the fix.
…the lockfile

The version-traversal fix validated versions only after normalization, but the
legacy manifest-migration path writes pnpm-lock.yaml (and pacquet records lockfile
entries and writes them) before that gate ran — so a traversal-shaped version
extracted from the inline `<version>+<integrity>` form could be persisted before
being rejected.

Validate the migrated version as exact semver inside the migration, before any
lockfile or settings write. The TS version check moved to a shared
`assertValidConfigDepVersion` module so both `installConfigDeps` and
`migrateConfigDepsToLockfile` use it without a circular import; pacquet's
`assert_valid_config_dep_version` is now called from the migrate branches too.
Regression tests on both stacks assert no pnpm-lock.yaml is written for an invalid
migrated version; verified each fails without the fix.
…ssage

Lock the rendered `InvalidConfigDepVersion` message with an exact-string
assertion. The version is wrapped in single quotes (`... invalid version
"<version>"`); the assertion guards against a doubled or dropped quote so the
diagnostic stays well-formed.
Remove comments that restated the code or narrated test scenarios (tests are
documentation), drop call-site comments that duplicated the callee's doc, and
collapse the repeated security rationale to a single concise note per validator.
Keep only the genuinely non-obvious "why": the store-path-segment reason for
validating names/versions, the null-prototype `__proto__` gotcha, the
symlink-skip in the search helper, the JSON.parse-own-key test mechanic, and the
pre-write ordering.
…yEnvLockfile

Following the convention of the main lockfile's verifyLockfileResolutions (whose
always-on offline checks already validate dependency aliases), introduce a single
verifyEnvLockfile / verify_env_lockfile that validates every config-dependency and
optional-subdependency name and version on the env lockfile before any path is
built or written.

This replaces the scattered per-call-site checks (assertValidRawConfigDepNames,
assertValidConfigDeps, the migrate-path version checks, and the pacquet
equivalents) with one named gate invoked where the env lockfile materializes or
just before each write — which also keeps the "no pre-rejection write" guarantee
structurally. Behavior is unchanged; the existing traversal/__proto__/version and
pre-write regression tests pass on both stacks.
…ied-write seam

The pre-write verifyEnvLockfile calls were repeated at every persistence site. Add
a single writeVerifiedEnvLockfile / write_verified_env_lockfile that verifies then
writes, and route every env-lockfile write through it. This removes the four
scattered pre-write checks, makes "an env lockfile is never persisted unverified"
a structural guarantee rather than a convention, and closes a gap where
resolvePackageManagerIntegrities wrote without verifying.

Only two explicit verifyEnvLockfile calls remain — at the install boundary, where
a committed lockfile is installed without being rewritten (a genuinely distinct
trust point from persistence). Behavior is unchanged; all tests pass on both stacks.
The note explained that the manifest write doesn't run when writeVerifiedEnvLockfile
throws — which is just how `await` works. The function name already says it verifies.
Comment thread installing/env-installer/src/verifyEnvLockfile.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 863b24c

Trim the doc comments on the env-lockfile verifier, the verified-write seam, the
version validator, and the related tests down to the non-obvious why, and drop a
call-site comment that only restated `await` sequencing.
@zkochan zkochan force-pushed the fix/config-deps-path-traversal-ghsa branch from 863b24c to 1b9a673 Compare June 17, 2026 21:35
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1b9a673

@zkochan zkochan enabled auto-merge (squash) June 17, 2026 22:45
@zkochan zkochan merged commit bee4bf4 into main Jun 17, 2026
31 checks passed
@zkochan zkochan deleted the fix/config-deps-path-traversal-ghsa branch June 17, 2026 23:03
zkochan added a commit that referenced this pull request Jun 18, 2026
…12504)

Backports three merged security fixes from main to release/10:

- Contain hoisted dependency aliases so a crafted lockfile alias cannot
  escape node_modules or overwrite pnpm-owned layout. The hoisted graph
  builder now validates each alias at the safeJoinModulesDir sink.
  (GHSA-fr4h-3cph-29xv, #12343)
- Contain pnpm patch-remove deletions to the configured patches
  directory. (#12341)
- Reject path-traversal config dependency names and versions from
  pnpm-workspace.yaml before they are used to build filesystem paths.
  (GHSA-qrv3-253h-g69c, #12470)

Written by an agent (Claude Code, claude-opus-4-8).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pacquet product: pnpm@11 reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants