Skip to content

fix(security): harden build-approval artifact identities on v10#12306

Merged
zkochan merged 3 commits into
release/10from
backport/v10-allowbuilds-deppath
Jun 10, 2026
Merged

fix(security): harden build-approval artifact identities on v10#12306
zkochan merged 3 commits into
release/10from
backport/v10-allowbuilds-deppath

Conversation

@zkochan

@zkochan zkochan commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Port of #12294 (commit bf1b731ee6) to release/10.

Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer approve lifecycle scripts for artifacts whose identity a name cannot pin: git, git-hosted tarball, direct tarball, and local directory dependencies. A dependency claiming the name of an allow-listed package (e.g. a git dependency whose manifest says "name": "esbuild") could previously get its scripts approved by that name. To approve such an artifact explicitly, use its peer-suffix-free lockfile depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm ignored-builds, and pnpm approve-builds print exactly that key.

  • AllowBuild policy functions identify packages by DepPath instead of caller-supplied name/version. The policy parses name and version out of the depPath itself, so name-keyed rules can never be fed an identity that disagrees with the gated artifact.
  • Identity trust is derived from the depPath shape: a registry-style depPath (name@semver) is a trusted identity. This is sound because lockfile entries are structurally checked wherever they are materialized into fetchable resolutions (pkgSnapshotToResolution, which covers the headless isolated/hoisted graphs and the non-headless lockfile-reuse path) and in the rebuild loop before scripts run: a registry-style key backed by a git, directory, or git-hosted tarball resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH. Non-http(s)-schemed and host-escaping tarball URLs under semver keys are rejected too; registry-relative tarball paths written by older pnpm versions stay accepted.
  • preparePackage always treats the fetched manifest as an untrusted identity: it requires a pkgResolutionId and gates on the synthesized name@<resolution id> depPath. scp-style git URLs are normalized to ssh:// form in resolution ids, and the git fetcher reuses createGitHostedPkgId from the resolver instead of re-deriving ids.
  • pnpm rebuild and pnpm approve-builds accept depPath specs for selecting and approving artifact builds; installs rebuild previously ignored builds approved by depPath keys (runUnignoredDependencyBuilds).
  • isGitHostedTarballUrl matches case-insensitively and is shared from @pnpm/lockfile.utils (the lockfile.fs copy is removed). New shared helpers: removePeersSuffix() in @pnpm/dependency-path and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
  • shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p / CVE-2026-9277).

Differences from main

v10 has no lockfile resolution verifier (verifyLockfileResolutions), so the structural shape pass lives in pkgSnapshotToResolution and the rebuild loop instead of the verification gate, and there is no verification-cache identity to thread. The AllowBuild policy keeps v10's boolean return (no explicit-deny tri-state) and is built from onlyBuiltDependencies/neverBuiltDependencies, into which allowBuilds is folded by config. The revoked-approval detection and the global-virtual-store hash/rebuild-projection changes from main have no v10 counterpart.

Testing

Ported/adapted the policy, prepare-package, fetcher, git-resolver, dlx, and core lifecycle-script tests from main, and added a v10 test suite for the new structural check (lockfile/utils/test/assertRegistryShapedResolution.ts). Suites for all touched packages pass locally except failures that reproduce identically on the untouched release/10 tip in the same environment (rebuild/build-commands/headless spawn-related local-env issues).


Written by an agent (Claude Code, claude-fable-5).

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a8a833d-2788-4e48-9d7c-be363a488436

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backport/v10-allowbuilds-deppath

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.

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. File-based approvals ignored 🐞 Bug ≡ Correctness ⭐ New
Description
runUnignoredDependencyBuilds() returns early when opts.onlyBuiltDependencies is empty, so
approvals provided only via onlyBuiltDependenciesFile are never applied to previously-ignored
builds during install.
Code

pkg-manager/core/src/install/index.ts[R870-876]

  if (!opts.onlyBuiltDependencies?.length) {
    return previousIgnoredBuilds
  }
-  const onlyBuiltDeps = createPackageVersionPolicy(opts.onlyBuiltDependencies)
-  const pkgsToBuild = Array.from(previousIgnoredBuilds).flatMap((ignoredPkg) => {
-    const ignoredPkgName = dp.parse(ignoredPkg).name
-    if (!ignoredPkgName) return []
-    const matchResult = onlyBuiltDeps(ignoredPkgName)
-    if (matchResult === true) {
-      return [ignoredPkgName]
-    } else if (Array.isArray(matchResult)) {
-      return matchResult.map(version => `${ignoredPkgName}@${version}`)
+  const allowBuild = createAllowBuildFunction(opts)
+  if (!allowBuild) {
+    return previousIgnoredBuilds
+  }
Evidence
The install path short-circuits solely on onlyBuiltDependencies length, even though install
options and the policy builder both support onlyBuiltDependenciesFile; therefore file-only
allowlists are ignored when deciding which previously-ignored builds to rebuild.

pkg-manager/core/src/install/index.ts[869-876]
pkg-manager/core/src/install/extendInstallOptions.ts[83-87]
builder/policy/src/index.ts[15-19]

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

### Issue description
`runUnignoredDependencyBuilds()` bails out if `opts.onlyBuiltDependencies?.length` is falsy, which skips the policy evaluation entirely. This breaks the configuration path where approvals are provided via `onlyBuiltDependenciesFile` (since `createAllowBuildFunction()` *does* read that file).

### Issue Context
- `StrictInstallOptions` supports `onlyBuiltDependenciesFile`.
- `createAllowBuildFunction()` supports `onlyBuiltDependenciesFile`.
- But `runUnignoredDependencyBuilds()` currently checks only `onlyBuiltDependencies?.length` before calling `createAllowBuildFunction()`.

### Fix Focus Areas
- Update the early-return condition to also consider `onlyBuiltDependenciesFile`, or move the early-return to after `createAllowBuildFunction()`.

- pkg-manager/core/src/install/index.ts[869-882]

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


2. Null resolution treated trusted 🐞 Bug ⛨ Security
Description
isRegistryShapedResolution() returns true when pkgSnapshot.resolution is null/undefined, so a
registry-style depPath can pass assertRegistryShapedResolution() even when the lockfile entry is
malformed. This bypasses the intended ERR_PNPM_RESOLUTION_SHAPE_MISMATCH protection and can lead
to later crashes when other code assumes resolution is an object.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-40]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
Evidence
The new guard explicitly treats resolution == null as registry-shaped, even though
PackageSnapshot.resolution is required by the lockfile types and downstream code assumes it is an
object and reads properties like type/integrity after the assertion runs.

lockfile/utils/src/assertRegistryShapedResolution.ts[28-40]
lockfile/types/src/index.ts[41-50]
lockfile/utils/src/pkgSnapshotToResolution.ts[15-33]
exec/plugin-commands-rebuild/src/implementation/index.ts[399-404]

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

## Issue description
`assertRegistryShapedResolution()` is intended to enforce that registry-shaped depPaths (`name@semver`) are backed by registry-shaped resolutions. However, `isRegistryShapedResolution()` currently treats `null`/`undefined` `resolution` values as registry-shaped (`return true`), which allows malformed/tampered lockfile entries to bypass the invariant check.
### Issue Context
- The lockfile is parsed from YAML without schema validation, so missing fields are possible.
- Other code paths (e.g. `pkgSnapshotToResolution`) immediately access `resolution.type` / `resolution.integrity`, which will throw if `resolution` is missing.
### Fix Focus Areas
- Make `isRegistryShapedResolution()` return `false` (or throw a targeted `PnpmError`) when `resolution == null` for registry-style depPaths.
- Ensure the `variations` branch also fails closed if any variant has a missing/invalid `resolution`.
- lockfile/utils/src/assertRegistryShapedResolution.ts[38-50]

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


3. Quadratic depPath matching 🐞 Bug ➹ Performance
Description
rebuildSelectedPkgs() calls matchesDepPath() for every pkg spec, and matchesDepPath() linearly
scans all lockfile package keys each time, making selection O(M×N). This can significantly slow
rebuild/installs when many packages are passed to rebuild (e.g. replaying lots of previously-ignored
builds) on large lockfiles.
Code

exec/plugin-commands-rebuild/src/implementation/index.ts[R175-178]

+function matchesDepPath (packages: PackageSnapshots, pkgSpec: string): boolean {
+  const normalizedPkgSpec = dp.removePeersSuffix(pkgSpec)
+  return Object.keys(packages).some((depPath) => dp.removePeersSuffix(depPath) === normalizedPkgSpec)
+}
Evidence
The rebuild selector creation calls matchesDepPath(packages, arg) for each arg, and
matchesDepPath() scans all package keys via Object.keys(packages).some(...), so runtime grows
with both the number of args and the number of lockfile entries; runUnignoredDependencyBuilds()
can supply many pkg specs into rebuildSelectedPkgs() when replaying previously ignored builds.

exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]
pkg-manager/core/src/install/index.ts[869-889]

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

## Issue description
`rebuildSelectedPkgs()` determines whether each CLI arg is a depPath by calling `matchesDepPath(packages, arg)`, which currently scans `Object.keys(packages)` for every arg. This creates O(pkgSpecs × packageCount) behavior and can become a bottleneck on large lockfiles or when many ignored builds are replayed.
### Issue Context
- `matchesDepPath()` is called from `pkgSpecs.map(...)`.
- `matchesDepPath()` currently does `Object.keys(packages).some(...)`, repeatedly re-iterating the full package key list.
### Fix Focus Areas
- Build a `Set` (or `Map`) of normalized depPaths once (e.g. `new Set(Object.keys(packages).map(dp.removePeersSuffix))`) and then make `matchesDepPath()` an O(1) membership check.
- Ensure normalization is consistent with the selector comparison logic (peer suffix stripped, patch hash preserved).
- exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]

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


View more (1)
4. Non-string tarball bypasses check 🐞 Bug ☼ Reliability
Description
assertRegistryShapedResolution() treats any non-empty non-string resolution.tarball as
registry-shaped, so a corrupted/tampered lockfile can bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
Downstream code assumes tarball is string-like (e.g. calls startsWith), which can turn
corruption into a runtime TypeError instead of a clear validation error.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-77]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, tarball, variants } = resolution as {
+    type?: unknown
+    gitHosted?: unknown
+    tarball?: unknown
+    variants?: unknown
+  }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  // Plain tarball / registry resolution. The lockfile is parsed from YAML
+  // without schema validation, so the `gitHosted` flag is not trustworthy on
+  // its own: a tampered entry could set a non-boolean (dodging a strict
+  // `=== true`) or an explicit `false` on a git-host URL (the loader only
+  // backfills the flag when absent). Treat any non-boolean flag as git-hosted
+  // and gate on the URL so the verdict never depends on the flag alone.
+  if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted)) return false
+  // A registry resolution reconstructs its tarball URL from name+version, so
+  // an absent/empty `tarball` is registry-shaped. When a URL with a scheme is
+  // present it must be an http(s) artifact: a `file:` tarball under a
+  // name@semver key is a local artifact that a package-name rule must not
+  // approve.
+  if (typeof tarball === 'string' && tarball !== '') {
+    if (hasUrlScheme(tarball)) {
+      if (!/^https?:\/\//i.test(tarball)) return false
+      if (isGitHostedTarballUrl(tarball)) return false
+    } else if (tarball.startsWith('/') || tarball.startsWith('\\')) {
+      // Protocol-relative and path-absolute forms (`//host`, `/\host`, ...)
+      // can escape the configured registry host when resolved as a URL.
+      return false
+    }
+    // A scheme-less relative path is resolved against the configured
+    // registry, so it cannot point off-registry.
+  }
+  return true
Evidence
The new shape check only inspects tarball when it is a non-empty string, so a non-string tarball
value passes as registry-shaped. pkgSnapshotToResolution() subsequently uses string methods on
resolution.tarball (and passes it to URL-related checks), which will throw if tarball is not
actually a string.

lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
lockfile/utils/src/pkgSnapshotToResolution.ts[28-33]

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

## Issue description
`isRegistryShapedResolution()` only validates `tarball` when it is a non-empty string. If `tarball` is present but not a string (e.g. number/object due to lockfile corruption/tampering), the function returns `true` and `assertRegistryShapedResolution()` will not throw `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`. Later code paths treat `tarball` as a string and may throw a `TypeError`, yielding a less actionable failure mode and weakening the new fail-closed invariant.
### Issue Context
This PR introduces offline shape validation to ensure registry-shaped depPaths are backed by registry-shaped resolutions. Since lockfiles are parsed from YAML without schema validation, the shape check should also reject structurally invalid field types (like non-string `tarball`) rather than letting them pass.
### Fix Focus Areas
- lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
### Suggested fix
- In `isRegistryShapedResolution()`, add a guard such as:
- `if (tarball != null && typeof tarball !== 'string') return false`
- (Optional but aligned) consider similar strictness for other inspected fields (`variants`, etc.) to keep the invariant “shape check fails closed on malformed data.”

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


Grey Divider

Previous review results

Review updated until commit a010c4d

Results up to commit 4c9251c


🐞 Bugs (3) 📘 Rule violations (0)


Remediation recommended
1. Null resolution treated trusted 🐞 Bug ⛨ Security ⭐ New
Description
isRegistryShapedResolution() returns true when pkgSnapshot.resolution is null/undefined, so a
registry-style depPath can pass assertRegistryShapedResolution() even when the lockfile entry is
malformed. This bypasses the intended ERR_PNPM_RESOLUTION_SHAPE_MISMATCH protection and can lead
to later crashes when other code assumes resolution is an object.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-40]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
Evidence
The new guard explicitly treats resolution == null as registry-shaped, even though
PackageSnapshot.resolution is required by the lockfile types and downstream code assumes it is an
object and reads properties like type/integrity after the assertion runs.

lockfile/utils/src/assertRegistryShapedResolution.ts[28-40]
lockfile/types/src/index.ts[41-50]
lockfile/utils/src/pkgSnapshotToResolution.ts[15-33]
exec/plugin-commands-rebuild/src/implementation/index.ts[399-404]

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

### Issue description
`assertRegistryShapedResolution()` is intended to enforce that registry-shaped depPaths (`name@semver`) are backed by registry-shaped resolutions. However, `isRegistryShapedResolution()` currently treats `null`/`undefined` `resolution` values as registry-shaped (`return true`), which allows malformed/tampered lockfile entries to bypass the invariant check.

### Issue Context
- The lockfile is parsed from YAML without schema validation, so missing fields are possible.
- Other code paths (e.g. `pkgSnapshotToResolution`) immediately access `resolution.type` / `resolution.integrity`, which will throw if `resolution` is missing.

### Fix Focus Areas
- Make `isRegistryShapedResolution()` return `false` (or throw a targeted `PnpmError`) when `resolution == null` for registry-style depPaths.
- Ensure the `variations` branch also fails closed if any variant has a missing/invalid `resolution`.

- lockfile/utils/src/assertRegistryShapedResolution.ts[38-50]

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


2. Quadratic depPath matching 🐞 Bug ➹ Performance
Description
rebuildSelectedPkgs() calls matchesDepPath() for every pkg spec, and matchesDepPath() linearly
scans all lockfile package keys each time, making selection O(M×N). This can significantly slow
rebuild/installs when many packages are passed to rebuild (e.g. replaying lots of previously-ignored
builds) on large lockfiles.
Code

exec/plugin-commands-rebuild/src/implementation/index.ts[R175-178]

+function matchesDepPath (packages: PackageSnapshots, pkgSpec: string): boolean {
+  const normalizedPkgSpec = dp.removePeersSuffix(pkgSpec)
+  return Object.keys(packages).some((depPath) => dp.removePeersSuffix(depPath) === normalizedPkgSpec)
+}
Evidence
The rebuild selector creation calls matchesDepPath(packages, arg) for each arg, and
matchesDepPath() scans all package keys via Object.keys(packages).some(...), so runtime grows
with both the number of args and the number of lockfile entries; runUnignoredDependencyBuilds()
can supply many pkg specs into rebuildSelectedPkgs() when replaying previously ignored builds.

exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]
pkg-manager/core/src/install/index.ts[869-889]

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

## Issue description
`rebuildSelectedPkgs()` determines whether each CLI arg is a depPath by calling `matchesDepPath(packages, arg)`, which currently scans `Object.keys(packages)` for every arg. This creates O(pkgSpecs × packageCount) behavior and can become a bottleneck on large lockfiles or when many ignored builds are replayed.
### Issue Context
- `matchesDepPath()` is called from `pkgSpecs.map(...)`.
- `matchesDepPath()` currently does `Object.keys(packages).some(...)`, repeatedly re-iterating the full package key list.
### Fix Focus Areas
- Build a `Set` (or `Map`) of normalized depPaths once (e.g. `new Set(Object.keys(packages).map(dp.removePeersSuffix))`) and then make `matchesDepPath()` an O(1) membership check.
- Ensure normalization is consistent with the selector comparison logic (peer suffix stripped, patch hash preserved).
- exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]

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


3. Non-string tarball bypasses check 🐞 Bug ☼ Reliability
Description
assertRegistryShapedResolution() treats any non-empty non-string resolution.tarball as
registry-shaped, so a corrupted/tampered lockfile can bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
Downstream code assumes tarball is string-like (e.g. calls startsWith), which can turn
corruption into a runtime TypeError instead of a clear validation error.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-77]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, tarball, variants } = resolution as {
+    type?: unknown
+    gitHosted?: unknown
+    tarball?: unknown
+    variants?: unknown
+  }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  // Plain tarball / registry resolution. The lockfile is parsed from YAML
+  // without schema validation, so the `gitHosted` flag is not trustworthy on
+  // its own: a tampered entry could set a non-boolean (dodging a strict
+  // `=== true`) or an explicit `false` on a git-host URL (the loader only
+  // backfills the flag when absent). Treat any non-boolean flag as git-hosted
+  // and gate on the URL so the verdict never depends on the flag alone.
+  if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted)) return false
+  // A registry resolution reconstructs its tarball URL from name+version, so
+  // an absent/empty `tarball` is registry-shaped. When a URL with a scheme is
+  // present it must be an http(s) artifact: a `file:` tarball under a
+  // name@semver key is a local artifact that a package-name rule must not
+  // approve.
+  if (typeof tarball === 'string' && tarball !== '') {
+    if (hasUrlScheme(tarball)) {
+      if (!/^https?:\/\//i.test(tarball)) return false
+      if (isGitHostedTarballUrl(tarball)) return false
+    } else if (tarball.startsWith('/') || tarball.startsWith('\\')) {
+      // Protocol-relative and path-absolute forms (`//host`, `/\host`, ...)
+      // can escape the configured registry host when resolved as a URL.
+      return false
+    }
+    // A scheme-less relative path is resolved against the configured
+    // registry, so it cannot point off-registry.
+  }
+  return true
Evidence
The new shape check only inspects tarball when it is a non-empty string, so a non-string tarball
value passes as registry-shaped. pkgSnapshotToResolution() subsequently uses string methods on
resolution.tarball (and passes it to URL-related checks), which will throw if tarball is not
actually a string.

lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
lockfile/utils/src/pkgSnapshotToResolution.ts[28-33]

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

## Issue description
`isRegistryShapedResolution()` only validates `tarball` when it is a non-empty string. If `tarball` is present but not a string (e.g. number/object due to lockfile corruption/tampering), the function returns `true` and `assertRegistryShapedResolution()` will not throw `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`. Later code paths treat `tarball` as a string and may throw a `TypeError`, yielding a less actionable failure mode and weakening the new fail-closed invariant.
### Issue Context
This PR introduces offline shape validation to ensure registry-shaped depPaths are backed by registry-shaped resolutions. Since lockfiles are parsed from YAML without schema validation, the shape check should also reject structurally invalid field types (like non-string `tarball`) rather than letting them pass.
### Fix Focus Areas
- lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
### Suggested fix
- In `isRegistryShapedResolution()`, add a guard such as:
- `if (tarball != null && typeof tarball !== 'string') return false`
- (Optional but aligned) consider similar strictness for other inspected fields (`variants`, etc.) to keep the invariant “shape check fails closed on malformed data.”

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


Results up to commit 7288c44


🐞 Bugs (2) 📘 Rule violations (0)


Remediation recommended
1. Quadratic depPath matching 🐞 Bug ➹ Performance ⭐ New
Description
rebuildSelectedPkgs() calls matchesDepPath() for every pkg spec, and matchesDepPath() linearly
scans all lockfile package keys each time, making selection O(M×N). This can significantly slow
rebuild/installs when many packages are passed to rebuild (e.g. replaying lots of previously-ignored
builds) on large lockfiles.
Code

exec/plugin-commands-rebuild/src/implementation/index.ts[R175-178]

+function matchesDepPath (packages: PackageSnapshots, pkgSpec: string): boolean {
+  const normalizedPkgSpec = dp.removePeersSuffix(pkgSpec)
+  return Object.keys(packages).some((depPath) => dp.removePeersSuffix(depPath) === normalizedPkgSpec)
+}
Evidence
The rebuild selector creation calls matchesDepPath(packages, arg) for each arg, and
matchesDepPath() scans all package keys via Object.keys(packages).some(...), so runtime grows
with both the number of args and the number of lockfile entries; runUnignoredDependencyBuilds()
can supply many pkg specs into rebuildSelectedPkgs() when replaying previously ignored builds.

exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]
pkg-manager/core/src/install/index.ts[869-889]

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

### Issue description
`rebuildSelectedPkgs()` determines whether each CLI arg is a depPath by calling `matchesDepPath(packages, arg)`, which currently scans `Object.keys(packages)` for every arg. This creates O(pkgSpecs × packageCount) behavior and can become a bottleneck on large lockfiles or when many ignored builds are replayed.

### Issue Context
- `matchesDepPath()` is called from `pkgSpecs.map(...)`.
- `matchesDepPath()` currently does `Object.keys(packages).some(...)`, repeatedly re-iterating the full package key list.

### Fix Focus Areas
- Build a `Set` (or `Map`) of normalized depPaths once (e.g. `new Set(Object.keys(packages).map(dp.removePeersSuffix))`) and then make `matchesDepPath()` an O(1) membership check.
- Ensure normalization is consistent with the selector comparison logic (peer suffix stripped, patch hash preserved).

- exec/plugin-commands-rebuild/src/implementation/index.ts[121-178]

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


2. Non-string tarball bypasses check 🐞 Bug ☼ Reliability
Description
assertRegistryShapedResolution() treats any non-empty non-string resolution.tarball as
registry-shaped, so a corrupted/tampered lockfile can bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
Downstream code assumes tarball is string-like (e.g. calls startsWith), which can turn
corruption into a runtime TypeError instead of a clear validation error.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-77]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, tarball, variants } = resolution as {
+    type?: unknown
+    gitHosted?: unknown
+    tarball?: unknown
+    variants?: unknown
+  }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  // Plain tarball / registry resolution. The lockfile is parsed from YAML
+  // without schema validation, so the `gitHosted` flag is not trustworthy on
+  // its own: a tampered entry could set a non-boolean (dodging a strict
+  // `=== true`) or an explicit `false` on a git-host URL (the loader only
+  // backfills the flag when absent). Treat any non-boolean flag as git-hosted
+  // and gate on the URL so the verdict never depends on the flag alone.
+  if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted)) return false
+  // A registry resolution reconstructs its tarball URL from name+version, so
+  // an absent/empty `tarball` is registry-shaped. When a URL with a scheme is
+  // present it must be an http(s) artifact: a `file:` tarball under a
+  // name@semver key is a local artifact that a package-name rule must not
+  // approve.
+  if (typeof tarball === 'string' && tarball !== '') {
+    if (hasUrlScheme(tarball)) {
+      if (!/^https?:\/\//i.test(tarball)) return false
+      if (isGitHostedTarballUrl(tarball)) return false
+    } else if (tarball.startsWith('/') || tarball.startsWith('\\')) {
+      // Protocol-relative and path-absolute forms (`//host`, `/\host`, ...)
+      // can escape the configured registry host when resolved as a URL.
+      return false
+    }
+    // A scheme-less relative path is resolved against the configured
+    // registry, so it cannot point off-registry.
+  }
+  return true
Evidence
The new shape check only inspects tarball when it is a non-empty string, so a non-string tarball
value passes as registry-shaped. pkgSnapshotToResolution() subsequently uses string methods on
resolution.tarball (and passes it to URL-related checks), which will throw if tarball is not
actually a string.

lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
lockfile/utils/src/pkgSnapshotToResolution.ts[28-33]

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

## Issue description
`isRegistryShapedResolution()` only validates `tarball` when it is a non-empty string. If `tarball` is present but not a string (e.g. number/object due to lockfile corruption/tampering), the function returns `true` and `assertRegistryShapedResolution()` will not throw `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`. Later code paths treat `tarball` as a string and may throw a `TypeError`, yielding a less actionable failure mode and weakening the new fail-closed invariant.
### Issue Context
This PR introduces offline shape validation to ensure registry-shaped depPaths are backed by registry-shaped resolutions. Since lockfiles are parsed from YAML without schema validation, the shape check should also reject structurally invalid field types (like non-string `tarball`) rather than letting them pass.
### Fix Focus Areas
- lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
### Suggested fix
- In `isRegistryShapedResolution()`, add a guard such as:
- `if (tarball != null && typeof tarball !== 'string') return false`
- (Optional but aligned) consider similar strictness for other inspected fields (`variants`, etc.) to keep the invariant “shape check fails closed on malformed data.”

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


Results up to commit 900b1b1


🐞 Bugs (1) 📘 Rule violations (0)


Remediation recommended
1. Non-string tarball bypasses check 🐞 Bug ☼ Reliability
Description
assertRegistryShapedResolution() treats any non-empty non-string resolution.tarball as
registry-shaped, so a corrupted/tampered lockfile can bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
Downstream code assumes tarball is string-like (e.g. calls startsWith), which can turn
corruption into a runtime TypeError instead of a clear validation error.
Code

lockfile/utils/src/assertRegistryShapedResolution.ts[R38-77]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, tarball, variants } = resolution as {
+    type?: unknown
+    gitHosted?: unknown
+    tarball?: unknown
+    variants?: unknown
+  }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  // Plain tarball / registry resolution. The lockfile is parsed from YAML
+  // without schema validation, so the `gitHosted` flag is not trustworthy on
+  // its own: a tampered entry could set a non-boolean (dodging a strict
+  // `=== true`) or an explicit `false` on a git-host URL (the loader only
+  // backfills the flag when absent). Treat any non-boolean flag as git-hosted
+  // and gate on the URL so the verdict never depends on the flag alone.
+  if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted)) return false
+  // A registry resolution reconstructs its tarball URL from name+version, so
+  // an absent/empty `tarball` is registry-shaped. When a URL with a scheme is
+  // present it must be an http(s) artifact: a `file:` tarball under a
+  // name@semver key is a local artifact that a package-name rule must not
+  // approve.
+  if (typeof tarball === 'string' && tarball !== '') {
+    if (hasUrlScheme(tarball)) {
+      if (!/^https?:\/\//i.test(tarball)) return false
+      if (isGitHostedTarballUrl(tarball)) return false
+    } else if (tarball.startsWith('/') || tarball.startsWith('\\')) {
+      // Protocol-relative and path-absolute forms (`//host`, `/\host`, ...)
+      // can escape the configured registry host when resolved as a URL.
+      return false
+    }
+    // A scheme-less relative path is resolved against the configured
+    // registry, so it cannot point off-registry.
+  }
+  return true
Evidence
The new shape check only inspects tarball when it is a non-empty string, so a non-string tarball
value passes as registry-shaped. pkgSnapshotToResolution() subsequently uses string methods on
resolution.tarball (and passes it to URL-related checks), which will throw if tarball is not
actually a string.

lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]
lockfile/utils/src/pkgSnapshotToResolution.ts[28-33]

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

### Issue description
`isRegistryShapedResolution()` only validates `tarball` when it is a non-empty string. If `tarball` is present but not a string (e.g. number/object due to lockfile corruption/tampering), the function returns `true` and `assertRegistryShapedResolution()` will not throw `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`. Later code paths treat `tarball` as a string and may throw a `TypeError`, yielding a less actionable failure mode and weakening the new fail-closed invariant.

### Issue Context
This PR introduces offline shape validation to ensure registry-shaped depPaths are backed by registry-shaped resolutions. Since lockfiles are parsed from YAML without schema validation, the shape check should also reject structurally invalid field types (like non-string `tarball`) rather than letting them pass.

### Fix Focus Areas
- lockfile/utils/src/assertRegistryShapedResolution.ts[38-77]

### Suggested fix
- In `isRegistryShapedResolution()`, add a guard such as:
 - `if (tarball != null && typeof tarball !== 'string') return false`
- (Optional but aligned) consider similar strictness for other inspected fields (`variants`, etc.) to keep the invariant “shape check fails closed on malformed data.”

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


Qodo Logo

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

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

Copy link
Copy Markdown

PR Summary by Qodo

Harden allowBuild/onlyBuiltDependencies by pinning approvals to depPath identity (v10)
🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Gate lifecycle-script approval on trusted depPath identity, not manifest-supplied package name.
• Reject lockfile entries where name@semver keys resolve to git/tarball/directory artifacts.
• Add depPath-based rebuild/approve flows, update tests, and pin shell-quote to 1.8.4.
Diagram
graph TD
  CLI["Install / Rebuild"] --> Policy["AllowBuild policy"] --> DepPath["DepPath utils"]
  CLI --> LockUtils["Lockfile shape check"] --> Lockfile[("pnpm-lock")]
  Fetchers["Git/Tarball fetchers"] --> Prepare["preparePackage"] --> Policy
  CLI --> Prepare
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralized lockfile verification phase (single gate)
  • ➕ One place to enforce depPath↔resolution invariants (simpler reasoning)
  • ➕ Potentially faster by caching verification results
  • ➖ Not available/consistent in v10 (no verifyLockfileResolutions pipeline here)
  • ➖ Still needs runtime enforcement near script execution to be robust
2. Require depPath approvals for all builds (drop name-based rules)
  • ➕ Eliminates ambiguity entirely; approvals always pin to an artifact identity
  • ➕ Simplifies policy logic (no trust inference from depPath shape)
  • ➖ Major UX regression for normal registry dependencies
  • ➖ Large config churn for existing users relying on package-name allowlists
3. Trust lockfile gitHosted flag / resolution metadata without URL inspection
  • ➕ Less code and fewer heuristics
  • ➕ Cheaper checks at runtime
  • ➖ Unsafe if lockfile is tampered (flag can be forged/typed incorrectly)
  • ➖ Would reintroduce the core confusion between name@semver keys and non-registry artifacts

Recommendation: Keep the PR’s approach: use depPath as the approval identity, infer trust only from registry-shaped depPaths, and enforce the depPath↔resolution shape invariant at materialization time (pkgSnapshotToResolution) and immediately before running scripts (rebuild). This preserves existing name-based allowlists for registry packages while closing the identity-confusion hole for git/tarball/directory artifacts, and it is compatible with v10’s architecture.

Grey Divider

File Changes

Enhancement (3)
package.json Add dependency-path dependency to builder.policy +1/-0

Add dependency-path dependency to builder.policy

• Adds @pnpm/dependency-path as a runtime dependency so the policy can parse and normalize depPaths.

builder/policy/package.json


index.ts Export assertRegistryShapedResolution and isGitHostedTarballUrl +1/-0

Export assertRegistryShapedResolution and isGitHostedTarballUrl

• Re-exports the new lockfile resolution-shape helpers for use by rebuild and lockfile conversion layers.

lockfile/utils/src/index.ts


index.ts Introduce removePeersSuffix() and reuse it for pkgIdWithPatchHash +9/-6

Introduce removePeersSuffix() and reuse it for pkgIdWithPatchHash

• Adds removePeersSuffix() to strip peer suffixes and simplifies getPkgIdWithPatchHash() to build on it, standardizing depPath normalization.

packages/dependency-path/src/index.ts


Bug fix (15)
index.ts Switch AllowBuild policy to depPath identity + add allowBuildKeyFromIgnoredBuild() +53/-4

Switch AllowBuild policy to depPath identity + add allowBuildKeyFromIgnoredBuild()

• Changes allowBuild evaluation from (name, version) to depPath and only applies name-based allowlists to registry-shaped depPaths. Adds helpers to derive the correct approval key for ignored builds and to detect depPath-style allowlist entries (including peer-suffix stripping and patch-hash preservation).

builder/policy/src/index.ts


getAutomaticallyIgnoredBuilds.ts Derive ignored-build display keys via allowBuildKeyFromIgnoredBuild() +2/-2

Derive ignored-build display keys via allowBuildKeyFromIgnoredBuild()

• Stops using parsed package name as the ignored-build key and instead uses the shared helper to return name for registry packages and depPath for artifacts.

exec/build-commands/src/getAutomaticallyIgnoredBuilds.ts


index.ts Call allowBuild with depPath when deciding to ignore scripts +1/-1

Call allowBuild with depPath when deciding to ignore scripts

• Updates build-modules to check build approval using depGraph node depPath rather than (name, version), aligning with the new AllowBuild type.

exec/build-modules/src/index.ts


index.ts Support depPath selectors in rebuild and assert lockfile resolution shape before scripts +23/-5

Support depPath selectors in rebuild and assert lockfile resolution shape before scripts

• Adds depPath-spec matching (peer-suffix-stripped and patch-hash-aware) for selecting packages to rebuild. Enforces assertRegistryShapedResolution before running lifecycle scripts and updates allowBuild usage to depPath-based identity.

exec/plugin-commands-rebuild/src/implementation/index.ts


index.ts Gate prepare scripts using synthesized depPath from pkgResolutionId +9/-4

Gate prepare scripts using synthesized depPath from pkgResolutionId

• Adds required pkgResolutionId to PreparePackageOptions and synthesizes depPath as name@pkgResolutionId for policy evaluation. Updates the error hint to instruct approving the artifact depPath key rather than package name.

exec/prepare-package/src/index.ts


index.ts Pass stable pkgResolutionId into preparePackage for git artifacts +2/-0

Pass stable pkgResolutionId into preparePackage for git artifacts

• Uses createGitHostedPkgId(resolution) to produce the pkgResolutionId and passes it to preparePackage so allowBuild gating uses an artifact identity.

fetching/git-fetcher/src/index.ts


gitHostedTarballFetcher.ts Pass pkgResolutionId for git-hosted tarballs into preparePackage +9/-0

Pass pkgResolutionId for git-hosted tarballs into preparePackage

• Adds a helper to build a stable pkgResolutionId from the tarball URL (and optional subpath) and passes it to preparePackage for depPath-based gating.

fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts


assertRegistryShapedResolution.ts Add registry depPath ↔ resolution shape invariant check +82/-0

Add registry depPath ↔ resolution shape invariant check

• Introduces assertRegistryShapedResolution() to reject name@semver depPaths backed by git/directory/git-hosted tarball/non-http(s) tarball resolutions, including case-insensitive git-host URL detection and host-escape tarball forms.

lockfile/utils/src/assertRegistryShapedResolution.ts


pkgSnapshotToResolution.ts Enforce registry-shaped resolution invariant during resolution materialization +2/-0

Enforce registry-shaped resolution invariant during resolution materialization

• Calls assertRegistryShapedResolution(depPath, pkgSnapshot) before turning lockfile snapshots into fetchable resolutions, ensuring identity trust holds in headless/hoisted and lockfile-reuse paths.

lockfile/utils/src/pkgSnapshotToResolution.ts


config.ts Change AllowBuild type to accept depPath instead of name/version +3/-1

Change AllowBuild type to accept depPath instead of name/version

• Updates the public AllowBuild function type to (depPath: DepPath) => boolean, forcing callers to use artifact identity rather than manifest fields.

packages/types/src/config.ts


index.ts Rebuild previously-ignored builds when approved by depPath keys +10/-13

Rebuild previously-ignored builds when approved by depPath keys

• Replaces version-policy matching with the unified createAllowBuildFunction() to decide which ignored builds to rebuild. Uses pkgIdWithPatchHash keys and updates ignored-build deduplication to preserve patch hashes.

pkg-manager/core/src/install/index.ts


link.ts Use depPath-based allowBuild when deciding side-effects cache eligibility +1/-1

Use depPath-based allowBuild when deciding side-effects cache eligibility

• Switches allowBuild checks from (name, version) to depPath when determining whether side-effects cache can be read for a node.

pkg-manager/core/src/install/link.ts


index.ts Use depPath-based allowBuild for side-effects cache in headless linker +1/-1

Use depPath-based allowBuild for side-effects cache in headless linker

• Updates headless linking logic to call allowBuild with depNode.depPath rather than (name, version).

pkg-manager/headless/src/index.ts


linkHoistedModules.ts Use depPath-based allowBuild for side-effects cache in hoisted linker +1/-1

Use depPath-based allowBuild for side-effects cache in hoisted linker

• Updates hoisted linking logic to call allowBuild with depNode.depPath rather than (name, version).

pkg-manager/headless/src/linkHoistedModules.ts


createGitHostedPkgId.ts Normalize scp-style git URLs to ssh:// in pkg resolution IDs +12/-1

Normalize scp-style git URLs to ssh:// in pkg resolution IDs

• Adds normalization for scp shorthand (user@host:path) while preserving already-schemed URLs (including ssh:// with ports) to avoid mangling.

resolving/git-resolver/src/createGitHostedPkgId.ts


Refactor (3)
package.json Use builder.policy for ignored-build key derivation +1/-1

Use builder.policy for ignored-build key derivation

• Adds @pnpm/builder.policy dependency and removes direct dependency on @pnpm/dependency-path for ignored-build handling.

exec/build-commands/package.json


package.json Depend on git-resolver to reuse createGitHostedPkgId() +1/-0

Depend on git-resolver to reuse createGitHostedPkgId()

• Adds @pnpm/git-resolver so the fetcher uses the same resolution-id derivation as the resolver.

fetching/git-fetcher/package.json


lockfileFormatConverters.ts Use shared isGitHostedTarballUrl() from lockfile.utils +1/-13

Use shared isGitHostedTarballUrl() from lockfile.utils

• Removes a local copy of git-hosted tarball URL detection and imports the shared helper from @pnpm/lockfile.utils.

lockfile/fs/src/lockfileFormatConverters.ts


Tests (9)
index.ts Expand policy tests for artifact depPaths, patch hashes, and ignored-build keys +75/-17

Expand policy tests for artifact depPaths, patch hashes, and ignored-build keys

• Updates tests to call allowBuild with depPaths and adds coverage for denying artifact depPaths by name, allowing by depPath key, preserving patch_hash, and allowBuildKeyFromIgnoredBuild behavior.

builder/policy/test/index.ts


dlx.e2e.ts Approve dlx git-hosted artifact by depPath, not name +3/-1

Approve dlx git-hosted artifact by depPath, not name

• Updates dlx git install test to pass an allowBuild entry matching the git-hosted tarball depPath key.

exec/plugin-commands-script-runners/test/dlx.e2e.ts


index.ts Add pkgResolutionId to preparePackage tests and verify depPath gating +16/-3

Add pkgResolutionId to preparePackage tests and verify depPath gating

• Updates tests to pass pkgResolutionId and adds a new case ensuring builds are denied when only registry-shaped depPaths are allowed.

exec/prepare-package/test/index.ts


index.ts Update git-fetcher tests to approve by depPath prefix +3/-3

Update git-fetcher tests to approve by depPath prefix

• Adjusts allowBuild test callbacks to check depPath prefixes (name@...) instead of just matching package name.

fetching/git-fetcher/test/index.ts


fetch.ts Update tarball-fetcher tests to approve by depPath prefix +1/-1

Update tarball-fetcher tests to approve by depPath prefix

• Adjusts allowBuild callback in git-hosted tarball prepare failure test to match on depPath identity rather than package name.

fetching/tarball-fetcher/test/fetch.ts


assertRegistryShapedResolution.ts Add tests for resolution-shape mismatch and git-host URL casing +145/-0

Add tests for resolution-shape mismatch and git-host URL casing

• Adds comprehensive unit tests covering mismatched resolution types, variations hiding non-registry variants, tarball scheme/escape edge cases, and case-insensitive git-host URL matching.

lockfile/utils/test/assertRegistryShapedResolution.ts


lifecycleScripts.ts Update allowlists for git-hosted artifacts and add direct-tarball regression test +36/-1

Update allowlists for git-hosted artifacts and add direct-tarball regression test

• Approves git-hosted dependencies via depPath and adds a test proving package-name allowlists do not approve direct tarball identities unless explicitly keyed by depPath.

pkg-manager/core/test/install/lifecycleScripts.ts


dlx.ts Update dlx tests to approve both registry and git-hosted shx +7/-1

Update dlx tests to approve both registry and git-hosted shx

• Passes multiple --allow-build entries: name-based approval for registry shx and depPath-based approval for the git-hosted tarball identity.

pnpm/test/dlx.ts


createGitHostedPkgId.test.ts Add coverage for ssh URLs with ports and scp shorthand normalization +4/-0

Add coverage for ssh URLs with ports and scp shorthand normalization

• Expands test matrix to ensure ssh URLs with ports are not rewritten and that scp shorthand is converted to ssh:// form consistently.

resolving/git-resolver/test/createGitHostedPkgId.test.ts


Documentation (1)
tough-allow-builds-identities.md Document depPath-based build approvals and lockfile shape mismatch error +19/-0

Document depPath-based build approvals and lockfile shape mismatch error

• Adds a changeset describing the new trusted-identity requirement for package-name allowlists. Documents depPath keys for artifact approvals and the new resolution shape mismatch rejection.

.changeset/tough-allow-builds-identities.md


Other (6)
tsconfig.json Wire dependency-path project reference into builder.policy +3/-0

Wire dependency-path project reference into builder.policy

• Adds the dependency-path workspace reference so TypeScript builds include the new policy dependency.

builder/policy/tsconfig.json


tsconfig.json Reorder project references for builder.policy dependency +3/-3

Reorder project references for builder.policy dependency

• Updates TS project references to include builder/policy and reflect dependency changes.

exec/build-commands/tsconfig.json


tsconfig.json Add git-resolver TS project reference +3/-0

Add git-resolver TS project reference

• Includes resolving/git-resolver in references to support the new createGitHostedPkgId import.

fetching/git-fetcher/tsconfig.json


pnpm-lock.yaml Pin shell-quote to 1.8.4 via overrides +10/-3

Pin shell-quote to 1.8.4 via overrides

• Adds an explicit override to force shell-quote 1.8.4 in addition to the existing range override, ensuring the patched version is selected.

pnpm-lock.yaml


pnpm-workspace.yaml Add workspace override for shell-quote 1.8.4 with CVE note +2/-0

Add workspace override for shell-quote 1.8.4 with CVE note

• Pins shell-quote to 1.8.4 in workspace overrides and documents the security advisory (GHSA-w7jw-789q-3m8p / CVE-2026-9277).

pnpm-workspace.yaml


package.json Add top-level override for shell-quote 1.8.4 +1/-0

Add top-level override for shell-quote 1.8.4

• Pins shell-quote to 1.8.4 in pnpm's package.json overrides to ensure consumers get the fixed version.

pnpm/package.json


Grey Divider

Qodo Logo

@zkochan zkochan marked this pull request as draft June 10, 2026 11:21
Port of #12294 (commit bf1b731) to release/10.

Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer
approve lifecycle scripts for artifacts whose identity a name cannot pin:
git, git-hosted tarball, direct tarball, and local directory dependencies.
To approve such an artifact explicitly, use its peer-suffix-free lockfile
depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm
ignored-builds, and pnpm approve-builds print exactly that key.

- AllowBuild policy functions identify packages by DepPath instead of
  caller-supplied name/version. Identity trust is derived from the depPath
  shape: a registry-style depPath (name@semver) is a trusted identity.
- The trust is sound because lockfile entries are structurally checked
  wherever they are materialized into fetchable resolutions
  (pkgSnapshotToResolution) and before rebuild runs scripts: a
  registry-style key backed by a git, directory, or git-hosted tarball
  resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
- preparePackage requires a pkgResolutionId and gates on the synthesized
  name@<resolution id> depPath; scp-style git URLs are normalized to
  ssh:// form in resolution ids and the git fetcher reuses
  createGitHostedPkgId from the resolver.
- isGitHostedTarballUrl matches case-insensitively and is shared from
  @pnpm/lockfile.utils; new removePeersSuffix() in @pnpm/dependency-path
  and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
- pnpm rebuild and approve-builds accept depPath specs for selecting and
  approving artifact builds; installs rebuild ignored builds approved by
  depPath keys.
- shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p /
  CVE-2026-9277).

Differences from main: v10 has no lockfile resolution verifier, so the
structural pass lives in pkgSnapshotToResolution and the rebuild loop; the
AllowBuild policy keeps v10's boolean return; the revoked-approval
detection and global-virtual-store hash changes have no v10 counterpart.
@zkochan zkochan force-pushed the backport/v10-allowbuilds-deppath branch from 900b1b1 to 7288c44 Compare June 10, 2026 11:32
@zkochan zkochan marked this pull request as ready for review June 10, 2026 11:37
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7288c44

The registry mock binds only localhost, so a hard-coded 127.0.0.1 URL is
refused in CI, and a tarball URL on the registry's own origin resolves as
a registry package, which defeats the direct-tarball identity the test
needs. An in-test HTTP server redirecting to the mock provides a separate
origin whose binding the test controls.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4c9251c

A git-hosted artifact has an untrusted package identity, so the
ajv-keywords build has to be approved by its depPath. Pin the fixture to
the commit the depPath names.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

@zkochan zkochan merged commit 14bceb1 into release/10 Jun 10, 2026
13 of 14 checks passed
@zkochan zkochan deleted the backport/v10-allowbuilds-deppath branch June 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant