Skip to content

fix(hosted): CRLF-safe yarn classic rewrite, guard npm: aliases - #188

Open
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/classic-hosted-crlf-alias
Open

fix(hosted): CRLF-safe yarn classic rewrite, guard npm: aliases#188
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/classic-hosted-crlf-alias

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

Four audit findings (2026-08-13 hosted-mode audit) against the yarn classic hosted rewriter, one critical:

  1. CRLF wrong-entry rewrite + false attestation (critical). rewrite_yarn_classic split blocks on "\n\n", which never matches in a CRLF yarn.lock (core.autocrlf Windows checkouts — yarn v1 parses them fine). The whole lock collapsed into ONE block, and the leftmost-match resolved/integrity replaces repointed the FIRST entry in the file at the patched artifact instead of the target. Concretely: a CRLF lock with abbrev@1.1.1 before left-pad@1.3.0 and a left-pad patch grant ends up installing the patched left-pad tarball AS abbrev (integrity passes — the pins are real), while left-pad keeps its registry URL and stays vulnerable. Downstream, the confirm gate (text.contains(artifact_url)) then counted the dep as redirected, ledgered it, and VEX-attested it — a full fail-open.

  2. Fork-alias hijack (medium). Block matching used a ^<name>@ regex on the key line, so a fork-substitution entry "left-pad@npm:totally-other@^1.3.0": — which resolves a different package — matched on the alias name and was silently rewritten onto the upstream patched artifact, reversing the user's deliberate fork pin.

  3. Alias-only consumers invisible (medium). An entry consuming the patched package only through an alias ("safe-pad@npm:left-pad@^1.3.0":) got no redirect and no signal at all in classic, and only a misleading generic entry_not_found in berry — that copy keeps installing the unpatched artifact with zero indication why.

  4. No entry-not-found warning in classic (low). Every sibling npm-family rewriter warns when a granted dep matches no lock entry (version drift, not installed); classic vanished silently.

Fix

  • CRLF: uniformly-CRLF locks are LF-normalized for block scanning and re-expanded on output, so the TARGET entry is rewritten and every untouched line stays byte-identical; ledger edit originals/new record the on-disk CRLF bytes so a future revert matches what the file really holds. LF locks are processed exactly as before (the existing basic golden and its expected-edits.json pass unmodified, preserving the cross-language TS edit-shape contract). Locks with bare carriage returns outside a CRLF pair are refused untouched, fail-closed, with redirect_yarn_classic_unsupported_line_endings.
  • Fork guard: block matching now resolves every comma-joined lock-key pattern to the real package it stands for via the vendored backend's proven pattern_real_name (visibility widened pub(super)pub(crate) instead of duplicating alias resolution), so <name>@npm:<other>@… fork blocks are never touched and surface as not-found.
  • Alias-only consumers: classic and berry both emit a specific redirect_yarn_classic_alias_skipped / redirect_yarn_berry_alias_skipped warning naming the entry (suppressing the misleading generic not-found) and the dep stays out of redirected/ledger/VEX via the existing confirm gate. Merged keys serving both a direct and an alias descriptor still rewrite. Rewriting alias entries to parity with the vendored classic backend is possible follow-up.
  • Not-found: classic now emits redirect_yarn_classic_entry_not_found on version drift/missing entry, mirroring berry/pnpm. Like berry's existing warning, it fires per npm dep absent from yarn.lock in mixed-lockfile projects — warning-hygiene consolidation is tracked separately.
  • docs/ecosystems.md documents the yarn npm: alias behavior for both flavors.

New golden fixtures (npm/yarn-classic/crlf, npm/yarn-classic/alias-guard) are covered by the pre-existing .gitattributes -text rule so their CRLF bytes survive checkout. They are Rust-side only for now: the depscan TS golden.test.ts twin needs the same fixtures/behavior ported (the TS rewriteYarnClassic likely shares the CRLF bug).

Testing

  • cargo test -p socket-patch-core --lib (2082 passed) — includes 7 new unit tests: CRLF target-only rewrite with byte-exact untouched lines + CRLF ledger originals + LF-equivalence, mixed-EOL fail-closed refusal, fork-alias no-hijack, alias-only skip warning (classic + berry), merged direct+alias non-regression, classic entry-not-found.
  • cargo test -p socket-patch-core --test redirect_golden — green, with the two new fixtures (both verified red against the pre-fix rewriter: the alias-guard fork block was visibly hijacked).
  • cargo test -p socket-patch-cli --test in_process_redirect (24 passed) — includes new full-chain scan --redirect test over a decoy-first CRLF classic lock: decoy untouched, target pinned, CRLF preserved, ledger written.
  • cargo test -p socket-patch-cli --test e2e_redirect_yarn_classic_build (7 passed, real corepack yarn@1.22.22 install-proof legs) and e2e_redirect_yarn_berry_build (7 passed).
  • All 5 new classic unit tests and the new fixtures verified to FAIL with the pre-fix rewriter swapped back in.

No findings skipped.

🤖 Generated with Claude Code


Note

Medium Risk
Changes hosted redirect logic for yarn classic and berry lockfiles, including a prior wrong-entry/attestation bug on CRLF locks; behavior is heavily tested but affects supply-chain pinning paths.

Overview
Critical CRLF fix: Classic yarn.lock hosted redirects now normalize CRLF to LF for block splitting, rewrite the correct target entry (not the first block), re-expand CRLF on output, and store CRLF bytes in revert ledger edits. Mixed line endings are left untouched with redirect_yarn_classic_unsupported_line_endings.

Matching & alias safety: Block selection uses pattern_real_name on comma-joined key patterns so fork entries like "left-pad@npm:other@…" are not hijacked. Alias-only consumers ("safe-pad@npm:left-pad@…") are skipped with explicit redirect_yarn_classic_alias_skipped / redirect_yarn_berry_alias_skipped warnings instead of silent failure or misleading entry_not_found. Classic now emits redirect_yarn_classic_entry_not_found when a granted dep has no matching entry.

Adds golden fixtures, unit tests, an in-process CRLF scan --redirect test, and docs/ecosystems.md notes on yarn npm: alias behavior.

Reviewed by Cursor Bugbot for commit ef92e46. Configure here.

On a CRLF yarn.lock (core.autocrlf Windows checkouts) the hosted
classic rewriter split blocks on "\n\n", which never matches CRLF
bytes: the whole lock became one block and the leftmost-match
replaces repointed the FIRST entry in the file at the patched
artifact instead of the target — with our real integrity pins, so
the install succeeded, the redirect was confirmed, ledgered, and
VEX-attested while the vulnerable dep kept its registry URL. The
rewriter now LF-normalizes CRLF locks for block scanning and
re-expands on output, so the correct entry is rewritten and every
untouched line stays byte-identical; ledger edits record the
on-disk CRLF form. Locks with bare carriage returns are refused
untouched with redirect_yarn_classic_unsupported_line_endings.

Block matching now resolves each lock-key pattern to the real
package it stands for (reusing the vendored backend's
pattern_real_name) instead of regex-matching the alias name, so a
fork-substitution entry ("left-pad@npm:other-pkg@^1") is never
hijacked onto the upstream patched artifact. Entries consuming the
patched package only through npm: aliases are skipped with an
explicit redirect_yarn_classic_alias_skipped /
redirect_yarn_berry_alias_skipped warning instead of silence (or
berry's misleading generic not-found), and classic now emits
redirect_yarn_classic_entry_not_found when a granted dep matches no
lock entry, matching every sibling npm-family rewriter.

Pinned by CRLF + alias-guard golden fixtures, rewriter unit tests,
and an in-process CRLF scan --redirect test.

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