Skip to content

fix(hosted): refuse pnpm v5/v6 lock keys fail-closed - #183

Open
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/pnpm-legacy-lock-grammars
Open

fix(hosted): refuse pnpm v5/v6 lock keys fail-closed#183
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/pnpm-legacy-lock-grammars

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

The hosted-mode pnpm rewriter's grammar only matches lockfileVersion 9 packages: keys (name@version, optionally quoted or /-prefixed). pnpm v6 embeds resolved peers in the key itself (/name@1.0.0(peer@2.0.0):) and v5.x uses path-style keys (/name/1.0.0:, peers suffixed _peer@ver), so those entries silently escaped the rewrite.

Audit findings addressed (2026-08-13 audit, both in crates/socket-patch-core/src/patch/redirect/mod.rs):

  • medium — v6 mixed plain+peered over-attests (silent fail-open). A v6 lock carrying BOTH /pkg@1.0.0: and /pkg@1.0.0(peer@2.0.0): had only the plain entry rewritten; matched_any became true so no warning fired, the dep was counted redirected, recorded, and attested via --vex — while every dependent resolving through the peered entry still installed the unpatched upstream tarball.
  • low — v6 peer-suffixed and v5.4 path-style keys never redirect. Pure-peered v6 and v5.x deps degraded to a bare redirect_pnpm_entry_not_found that never named the offending key, and docs/ecosystems.md claimed blanket pnpm-lock.yaml hosted support with no version caveat.

Fix

Fail closed at the rewriter boundary rather than extending the grammar: before rewriting each dep, a legacy-grammar detector scans every pnpm lock in the set for v5/v6 keys targeting that name@version. Any hit refuses the dep outright across ALL locks — nothing rewritten anywhere, so the dep is never confirmed, recorded, or attested — and emits a redirect_pnpm_unsupported_lock_key warning naming each unmatched key and the lock it lives in, with the remedy (regenerate with pnpm >= 9 and re-run).

  • The refusal also covers multi-lock sets: a dep present in a rewritable v9 lock AND a legacy lock (e.g. a Rush nested lock on old pnpm) is refused everywhere, since rewriting only the v9 lock would confirm the dep while the legacy lock keeps installing upstream.
  • v9 locks are untouched: v9 peer-suffixed snapshots: keys never start with / and carry no resolution:, so the detector cannot trip on them (verified by existing goldens and a guard test).
  • Plain v6 keys with no peered sibling for the target dep still rewrite, as before — the refusal does not overreach.
  • docs/ecosystems.md hosted matrix now states the pnpm v9 constraint, and a new pnpm bullet in the npm hosted-mode notes documents the refusal semantics.

Refusal was chosen over grammar extension because extending would require real-pnpm-generated v5/v6 fixtures plus a matching TS-backend rewriter change to preserve the shared-golden byte-consistency contract (cross-repo scope). Refusal is strictly stronger than the pre-fix behavior.

Known residual (rewriter-boundary limit): a lock already fail-open-rewritten by a pre-fix run still confirms on re-run, because the artifact URL is baked into the unchanged file and confirmation scans final texts; the new warning still fires naming the surviving legacy key.

Testing

  • New unit tests in patch::redirect::tests (all red-state verified — with the detection block removed, all four refusal tests fail on pre-fix code, reproducing the silent fail-open / bare entry-not-found behavior):
    • pnpm_v6_mixed_plain_and_peered_is_refused — the medium finding: no edits, no files, warning names the peered key, no entry_not_found
    • pnpm_v6_pure_peered_key_is_refused_by_name — scoped pure-peered v6 key named in the warning
    • pnpm_v5_path_style_keys_are_refused_by_name — both /name/version and _peer-suffixed v5.4 keys named
    • pnpm_legacy_lock_in_set_refuses_the_dep_everywhere — v9 root + legacy Rush nested lock: nothing rewritten anywhere; warning names key AND lock path
    • pnpm_v6_plain_key_without_peered_sibling_still_rewrites — guard against overreach
  • cargo test -p socket-patch-core --lib — 2080 passed
  • cargo test -p socket-patch-core --test redirect_golden — 1 passed (all shared goldens incl. npm/pnpm basic + nested-rush-lock byte-identical)
  • cargo test -p socket-patch-cli --test in_process_redirect_pnpm — 3 passed

No skipped findings.

🤖 Generated with Claude Code


Note

Medium Risk
Changes hosted npm redirect and attestation semantics for legacy pnpm locks—safer than partial rewrite, but operators on v5/v6 must upgrade locks to get redirects.

Overview
Hosted pnpm redirect used to only rewrite lockfileVersion 9 packages: keys. pnpm v6 can list the same dep as both /pkg@1.0.0: and /pkg@1.0.0(peer@2.0.0):; rewriting only the plain entry could mark the dep redirected/attested while peered resolution still pulled the upstream tarball.

Before rewriting each dep, the rewriter scans all pnpm-lock.yaml files for v5/v6 keys that target that name@version (v6 peer-in-key and v5 path-style /name/version / _peer suffixes). Any match refuses the whole dep—no lock in the set is edited—and emits redirect_pnpm_unsupported_lock_key naming each key and lock file, instead of redirect_pnpm_entry_not_found. Plain v6 keys with no legacy sibling for that dep still rewrite.

docs/ecosystems.md now states hosted pnpm is v9-only and documents the refusal behavior.

Reviewed by Cursor Bugbot for commit 93f86d5. Configure here.

The pnpm redirect grammar only matches lockfileVersion 9 packages
keys (name@version, optionally quoted or /-prefixed). pnpm v6 embeds
resolved peers in the key itself (/name@1.0.0(peer@2.0.0):) and v5.x
uses path-style keys (/name/1.0.0:, peers suffixed _peer@ver), so
those entries silently escaped the rewrite.

Worst case was fail-open: a v6 lock holding BOTH /pkg@1.0.0: and
/pkg@1.0.0(peer@2.0.0): got the plain entry rewritten, the dep was
counted redirected, recorded, and attested via --vex, while every
dependent resolving through the peered entry still installed the
unpatched upstream tarball with no warning at all. Pure-peered v6 and
v5.x deps degraded to a bare entry-not-found warning that never named
the offending key.

Detect v5/v6-grammar keys for the target name@version before
rewriting and refuse the dep outright across the whole lock set —
nothing rewritten, nothing confirmed — with a
redirect_pnpm_unsupported_lock_key warning naming each unmatched key
and the lock it lives in, plus the remedy (regenerate with pnpm >=9).
v9 locks are unaffected: their peer-suffixed snapshots: keys never
start with / and carry no resolution. The docs matrix now states the
pnpm v9 constraint for hosted mode.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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