Skip to content

fix(hosted): never redirect bundled npm entries; warn on misses - #189

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

fix(hosted): never redirect bundled npm entries; warn on misses#189
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/npm-hosted-inbundle-alias

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

Two audit findings (2026-08-13 hosted-mode audit) in the npm hosted rewriter (rewrite_npm_lock / rewrite_npm_v2_deps):

  1. Bundled entries were rewritten, confirmed, and VEX-attested though npm never installs them from the rewritten URL (high). inBundle: true entries in the v2/v3 packages map (and legacy bundled: true in the v1/v2 dependencies tree) are extracted from their parent's tarball at install time — npm ignores their resolved/integrity. The rewriter matched them anyway, so the hosted URL landed in the lockfile text, run_redirect's content-based confirmation gate counted the dep as redirected, persisted a ledger record, and --vex attested not_affected over bytes that never install. Concrete failure: a lock with the patched dep only as "node_modules/parent/node_modules/foo": { "inBundle": true, ... } reported redirected: 1 and produced a false VEX attestation while npm ci kept installing the unpatched bundled copy.

  2. Alias installs were silently dropped and the npm rewriter had no entry-not-found warning (medium). npm i my-alias@npm:foo@1.0.0 keys the lock entry by the alias with the real package in name. Discovery is alias-aware, but the rewriter matched only the key suffix, so the granted patch never landed — with redirected: 0 and an empty warnings array, unreadable in CI. The same key-only matching also meant a fork substitution (npm i foo@npm:other) could be hijacked back to the upstream patched artifact whenever the versions coincided.

Fix

The rewriter now mirrors the vendored backend's entry_name matching and refusal semantics, so both backends agree on what a lock entry stands for:

  • Entries are matched by the package they stand for: the explicit name field when present, else the path after the last node_modules/; keys outside node_modules/ (project root, workspace source dirs) are never candidates. Alias installs of the patched package now redirect; fork substitutions are never hijacked.
  • inBundle: true / legacy bundled: true entries are skipped with the loud stays-UNPATCHED warning redirect_npm_bundled_instance_skipped (same wording as vendor_bundled_instance_skipped). Because the confirmation gate is content-based, a skipped entry is consequently never counted redirected, never ledger-recorded, and never VEX-attested.
  • link: true entries are skipped with redirect_npm_link_entry_skipped.
  • A granted dep matching no lock entry warns redirect_npm_entry_not_found — parity with the pnpm/berry/uv rewriters — covering the version-drift and not-installed cases.

Skips set matched_any, so the specific loud warning fires instead of a misleading not-found. For name-less entries the matching is behaviorally identical to the old key-suffix match (redirect goldens are byte-identical). Reruns stay idempotent: matched_any is set before the already-redirected short-circuit, so a second run neither warns nor re-edits.

Known accepted noise (pre-existing tradeoff, same as redirect_pnpm_entry_not_found): in a rare multi-lock project where the dep lives only in another PM's lockfile, redirect_npm_entry_not_found can fire spuriously; cross-lock warning hygiene is owned by the hosted-warnings lane. In v2 hybrid locks a bundled dep may warn twice (packages + dependencies trees), symmetric with the existing double-edit behavior for rewrites.

No findings skipped.

Testing

  • 6 new unit tests in patch::redirect::tests (bundled skip + loud warning, partial coverage with a rewritable sibling, legacy bundled: true, alias redirect, fork non-hijack, entry-not-found on version drift) — all RED-verified against the pre-fix rewriter, and independently mutation-checked in review (disabling the inBundle guard fails both bundled tests; removing name-aware matching fails both alias tests).
  • 1 new CLI subprocess test redirect_inbundle_only_dep_is_skipped_not_confirmed pinning the end-to-end invariant: --json envelope redirected == 0, bundled warning surfaced, package-lock.json byte-untouched, hosted URL absent.
  • Suites green: cargo test -p socket-patch-core --lib (2081), --test redirect_golden (byte-identical goldens), cargo test -p socket-patch-cli --test in_process_redirect (24), --test e2e_redirect_npm_build default legs and both --ignored real-npm legs (fresh npm ci installs patched bytes + VEX verifies; tampered tarball fails closed). Clippy -D warnings and rustfmt clean on touched targets/files.

🤖 Generated with Claude Code


Note

High Risk
Changes hosted redirect confirmation, ledger, and VEX semantics for npm lockfiles (bundled/alias/link cases); incorrect matching could still mis-redirect or miss patches in CI.

Overview
The npm hosted lock rewriter no longer treats every packages key suffix as the patched package. It matches entries by name when present (alias installs) or the path after the last node_modules/, and only considers keys under node_modules/ so workspace/root entries are not edited.

Bundled copies (inBundle: true / legacy bundled: true) and link: true entries are skipped with explicit warnings instead of rewriting resolved/integrity. That avoids counting a dep as redirected, ledger-recording it, or VEX-attesting when npm still installs unpatched bytes from a parent tarball.

When a granted override matches nothing in the lockfile, the rewriter now emits redirect_npm_entry_not_found, aligned with other PM rewriters. Unit tests and a CLI subprocess test cover bundled-only skips (redirected: 0, untouched lockfile).

Reviewed by Cursor Bugbot for commit cd5895a. Configure here.

Bundled (`inBundle`, legacy `bundled`) package-lock entries are
extracted from their parent's tarball at install time — npm ignores
their resolved/integrity — yet `scan --mode hosted` rewrote them
anyway. The hosted URL then appeared in the lockfile text, so the run
counted the dep as redirected, persisted a ledger record, and `--vex`
attested not_affected over bytes that never install. The npm rewriter
now skips bundled entries with the vendored backend's loud
stays-UNPATCHED warning (`redirect_npm_bundled_instance_skipped`), so
they are never rewritten, never counted redirected, and never
attested.

The rewriter also now matches lock entries by the package they stand
for — the entry's `name` field when present, mirroring the vendored
`entry_name` — so an alias install (`npm i alias@npm:real`) of the
patched package is redirected instead of silently dropped, and an
entry that merely shares the key name (`npm i <name>@npm:other`, the
fork-substitution pattern) is never hijacked to the upstream patched
artifact. Link (workspace) entries are skipped with a warning, and a
granted dep matching no lock entry finally warns
`redirect_npm_entry_not_found` — parity with the pnpm/berry/uv
rewriters — instead of vanishing from the redirected count with an
empty warnings array.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
tmp.path().join("package.json"),
r#"{ "name": "consumer", "version": "0.0.0", "dependencies": { "parent": "2.0.0" } }"#,
)
.unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You might want to replace usages of unwrap with expect in the future. expect lets you have a message specific to the error and helps in debugging.
ex. https://github.com/darakian/ddh/blob/aac9046fbfe302c64e180a8a88de3c262cd1a1a0/src/main.rs#L262

I assume this is claude'd code so probably not relevant for this PR but maybe something to encode in a skill.

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.

3 participants