Skip to content

fix(vendor): refuse catalog-consumed deps in pnpm vendoring - #184

Merged
Mikola Lysenko (mikolalysenko) merged 1 commit into
mainfrom
fix/pnpm-vendor-catalog-guard
Aug 14, 2026
Merged

fix(vendor): refuse catalog-consumed deps in pnpm vendoring#184
Mikola Lysenko (mikolalysenko) merged 1 commit into
mainfrom
fix/pnpm-vendor-catalog-guard

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

Audit finding (2026-08-13, vendored/pnpm, high): pnpm catalogs unguarded — a catalog-consumed dep's specifier: catalog: is rewritten to file:, breaking frozen installs.

For a dep consumed through a pnpm catalog (pnpm >= 9.5: pnpm-workspace.yaml catalog:), the lock importer entry is specifier: 'catalog:' / version: 1.3.0, mirrored by a top-level catalogs: snapshot section. edit_importers targeted on the version alone, so vendoring rewrote both importer fields to the file: spec while package.json kept its "left-pad": "catalog:" declaration and the catalogs: snapshot stayed stale. Vendoring reported success, but the very next pnpm install --frozen-lockfile rejected the lock with ERR_PNPM_OUTDATED_LOCKFILE (reproduced with real pnpm v11.21.0) — the user was handed a broken project with no warning. check_rewritable_refs already refused the sibling aliased and peer-suffixed spellings, but never looked at catalog wiring.

Fix

Refuse fail-closed rather than attempt a rewrite: the catalog range lives in pnpm-workspace.yaml, outside the package.json + pnpm-lock.yaml pair the surgery owns, so a consistent rewrite is out of reach at this boundary. check_rewritable_refs gains two guards, both firing pre-flight (vendor_lock_entry_unsupported) before anything is staged or written:

  1. A scan of the top-level catalogs: snapshot section, refusing when any catalog resolves the target name to the target version (peer-suffixed versions included).
  2. An importer-level check refusing when the dep entry at the target version carries a catalog: specifier — covering both the quoted default ('catalog:') and bare named (catalog:tools) spellings via a new unquote_value helper — so importer-only drifted locks refuse too.

The refusal detail names the catalog wiring and pnpm-workspace.yaml so the user knows why. Sibling versions resolved by a catalog do not block vendoring a plainly-consumed target.

No skipped findings — this lane had one finding and it is fully addressed.

Testing

  • New catalog_consumed_target_refuses_fail_closed: default-catalog, named-catalog, and importer-only drifted shapes (lock oracles generated by real pnpm v11.21.0); asserts the refusal code and detail, both files byte-untouched, and nothing staged under .socket/vendor. Red-verified against pre-fix code: without the guards the fixture vendors with Done (success=true) — the exact fail-broken success the audit described (independently re-verified during review).
  • New sibling_version_catalog_entry_does_not_block_vendoring: a catalog pinning 1.2.0 does not refuse a plainly-consumed 1.3.0 target, while the catalog-consumed 1.2.0 itself refuses.
  • Suites green: cargo test -p socket-patch-core --lib (2077 passed), cargo test -p socket-patch-cli --test e2e_vendor_pnpm_build (6 passed), cargo test -p socket-patch-cli --test in_process_vendor (27 passed). cargo fmt --check clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes pre-flight lock validation for pnpm vendoring; incorrect guards could block valid vendoring or miss unsupported shapes, but the change is fail-closed and scoped to unsupported catalog wiring.

Overview
pnpm pair surgery now refuses vendoring when the target dep is wired through a pnpm catalog (catalogs: section or importer specifier: catalog:…), returning vendor_lock_entry_unsupported before staging or writing anything.

Previously, rewriting importer fields to file: while package.json still declared catalog: and pnpm-workspace.yaml held the real range could report success and then fail pnpm install --frozen-lockfile with ERR_PNPM_OUTDATED_LOCKFILE.

check_rewritable_refs adds scans of the top-level catalogs: block (matching the target name/version, including peer-suffixed versions) and importer deps at the target version whose specifier starts with catalog: after a new unquote_value helper strips YAML quotes. Refusal messages mention catalog wiring and pnpm-workspace.yaml. A catalog on a sibling version does not block vendoring a plainly-consumed target at another version.

Tests cover default, named, and importer-only catalog shapes (real pnpm lock oracles), untouched files on refusal, and sibling-version isolation.

Reviewed by Cursor Bugbot for commit 03e2bd4. Configure here.

A dep consumed through a pnpm catalog (pnpm >= 9.5) carries
"specifier: catalog:" in the lock's importers while the real range
lives in pnpm-workspace.yaml, mirrored by a top-level "catalogs:"
snapshot section. The pair surgery matched importer entries on the
version alone, so it rewrote both importer fields to the file: spec
while package.json kept its "catalog:" specifier and the catalogs:
snapshot stayed stale. Vendoring reported success, but the very next
"pnpm install --frozen-lockfile" rejected the lock with
ERR_PNPM_OUTDATED_LOCKFILE (reproduced with real pnpm) — the user was
handed a broken project with no warning.

The catalog definition lives outside the package.json + pnpm-lock.yaml
pair this surgery owns, so a consistent rewrite is out of reach.
check_rewritable_refs now refuses fail-closed
(vendor_lock_entry_unsupported) before anything is staged or written,
whenever the target version is consumed through a catalog: specifier
in any importer or resolved by any catalogs: snapshot entry. The
refusal names the catalog wiring and pnpm-workspace.yaml so the user
knows why. Sibling versions resolved by a catalog do not block
vendoring a plainly-consumed target.

Regression tests pin the default-catalog ('catalog:'), named-catalog
(catalog:tools), and importer-only drifted shapes (lock oracles
generated by real pnpm), plus the sibling-version non-refusal.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit b107a3c into main Aug 14, 2026
231 of 234 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/pnpm-vendor-catalog-guard branch August 14, 2026 20:24
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.

2 participants