Repository navigation
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
Take a project whose dependent declares a registry range ("left-pad": "^1.3.0") and whose root package.json overrides it with a remote tarball URL:
"overrides": { "left-pad": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz" }npm records that copy in the lock with a resolved that looks exactly like a registry entry. scan --mode hosted and scan --mode vendored both rewire it and report success. But npm installs an overridden dependency from the override spec, not from the lock's resolved. So a fresh npm ci downloads the upstream URL and installs the unpatched bytes, with no error.
The guard for this case already exists. When a dependent declares the URL spec directly, hosted skips the copy with redirect_npm_non_registry_entry_skipped ("CANNOT be redirected") and vendored refuses it (verified in ledger run 62). Behind an override, that refusal doesn't fire.
Impact
- Vendored: after a fresh
npm ci, the tree is unpatched, yetvexexits 0 and writes an OpenVEX attestation for the patch. It warnsvendored_tree_out_of_syncand tells you to "re-run your package manager's install to resync it", and that remedy loops:npm installandnpm ciboth install the upstream bytes again, andnpm installalso reverts the lock'sresolvedto the registry.vendor --checkexits 0 ("committed artifact and wiring verified"). - Hosted: the scan says "Switched 1 package to hosted patches", and the in-run
scan --vexattestsnot_affected. A freshnpm cithen installs unpatched bytes. A standalonevexafterwards fails closed (not_applied, exit 1), so only the in-run attestation is wrong. - Both modes: the fix is silently never installed, while the scan exits 0 with
status: success.
Repro (Linux, Node 22.23.4, main cbbbdb9)
This needs a mock of the org patch API on 127.0.0.1:8899 serving one free patch for pkg:npm/left-pad@1.3.0, whose tarball prepends /* SOCKET-PATCHED */ to index.js. It uses the same contract as crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs: patches/batch, by-package, package (granted tarball + sha512), view/<uuid> and the tarball route.
#!/usr/bin/env bash
# repro.sh <npm-bin> <socket-patch-bin> <workdir> <hosted|vendored>
set -u; NPM=$1; SP=$2; W=$3; MODE=$4
rm -rf "$W"; mkdir -p "$W/foosrc" "$W/proj"
# a local tarball dependency `foo` that depends on left-pad ^1.3.0 (any transitive dependent works)
cd "$W/foosrc"
echo '{"name":"foo","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}' > package.json
echo 'module.exports=require("left-pad")' > index.js
$NPM pack --pack-destination "$W/proj" >/dev/null 2>&1
cd "$W/proj"
cat > package.json <<'EOF'
{"name":"app","version":"1.0.0","private":true,
"dependencies":{"foo":"file:foo-1.0.0.tgz"},
"overrides":{"left-pad":"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz"}}
EOF
# npm 12 only: echo 'allow-remote=all' > .npmrc (else npm refuses the URL override itself)
$NPM install >/dev/null 2>&1
API="--api-url http://127.0.0.1:8899 --org test-org --api-token fake --patch-server-url http://127.0.0.1:8899"
$SP scan --mode $MODE --yes $API; echo "scan exit=$?"
rm -rf node_modules; $NPM ci >/dev/null 2>&1; echo "npm ci exit=$?"
head -c 22 node_modules/left-pad/index.js | grep -q SOCKET && echo PATCHED || echo UNPATCHED
$SP vex $API >/dev/null; echo "vex exit=$?"
[ $MODE = vendored ] && { $SP vendor --check $API; echo "vendor --check exit=$?"; }Output (npm 10.9.9, vendored):
Vendored 1 package.
scan exit=0
npm ci exit=0
UNPATCHED
Warning: pkg:npm/left-pad@1.3.0: the installed tree does not match its vendored artifact; the attestation is based on the committed .socket/vendor artifact (the lockfile consumes it), but the live tree carries different bytes — re-run your package manager's install to resync it.
vex exit=0
pkg:npm/left-pad@1.3.0: committed artifact and wiring verified
vendor --check exit=0
Output (npm 10.9.9, hosted, with --json --vex in.vex.json):
scan: status success, warnings ['redirect_npm_allow_remote'] # no redirect_npm_non_registry_entry_skipped
in-run vex: not_affected
npm ci exit=0 → UNPATCHED
standalone vex exit=1 {'code': 'no_applicable_patches'} (not_applied)
The lock after the scan pins the copy as expected (resolved is the hosted URL in hosted mode, file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz in vendored mode). npm ignores that pin because the override spec wins.
Control: with the override set to a plain registry version ("left-pad": "1.3.0"), the same script gives PATCHED, and vex and vendor --check pass in both modes.
Expected vs actual
- Expected: the copy is treated like any other non-registry install. Hosted skips it with
redirect_npm_non_registry_entry_skipped, vendored refuses it, and no VEX attests it. That is the documented contract: docs/ecosystems.md (npm hosted notes) says a non-registry copy "keeps installing the user's own bytes …vexattests nothing for the package while the copy is in the lock", and the in-code comment atvendor/npm_origin.rs/patch/redirect/mod.rs:1571says "npm installs a git / url /file:dependency from the dependent's spec and ignoresresolved, so a rewrite here would confirm (and VEX-attest) a patch that never installs". - Actual: both modes rewire the copy and exit 0. After
npm cithe tree is unpatched. Vendoredvexandvendor --checkpass anyway, and the hosted in-run--vexattestsnot_affected.
OS × version
| OS | npm | lockfileVersion | hosted | vendored |
|---|---|---|---|---|
| Linux | 8.19.4 | 2 | repro (unpatched, standalone vex exit 1) |
repro (unpatched, vex 0, vendor --check 0) |
| Linux | 10.9.9 | 3 | repro (×3) | repro (×2) |
| Linux | 12.2.1 (allow-remote=all) |
3 | repro | repro |
| Linux | 12.2.1 (default allow-remote) |
n/a | n/a: npm itself refuses the URL override (EALLOWREMOTE) | n/a |
macOS and Windows weren't tested, because probe branches are blocked in this routine. The logic is platform-independent.
First bad version
This isn't a regression: v5.0.1 behaves the same (hosted and vendored, npm 10.9.9).
Suspect code
crates/socket-patch-core/src/vendor/npm_origin.rs:385, in npm_non_registry_entries. It skips every edge whose declared spec is a registry spec (if npm_spec_is_registry(spec) { continue; }) before it consults overrides. It only handles the opposite direction (#490: a non-registry spec that an override turns into a registry one, lines 390–394). A registry spec that an override replaces with a URL, git or file: spec falls through. Git and file: overrides are still caught by resolved_is_non_registry, because their resolved isn't a tarball URL. A remote-tarball URL override isn't caught, because its resolved is an ordinary https://…tgz. The same function feeds both the hosted rewriter (patch/redirect/mod.rs:1466) and the vendored writer (vendor/npm_lock.rs:1047), which is why both modes are affected. The probable fix is to also check overrides.replacement(...) for registry specs and treat a non-registry replacement as non-registry.
Related, but not duplicates: #490 (the opposite override direction), #1481 (file: copies), #326 (direct git / URL / file: specs).
Activity
npm bughunt run 63: file #1965
mikolalysenko commentedon Nov 2, 2026
[agent] Triage:
priority:p1(npm; a patch reported as installed and VEX-attested whilenpm ciinstalls the upstream bytes). Confirmed onmain@cbbbdb9:npm_non_registry_entries(crates/socket-patch-core/src/vendor/npm_origin.rs) skips every registry-spec edge before consultingoverrides, so a registry range overridden to a remote tarball URL is never marked non-registry. No duplicate or open PR found; related but distinct: #490 (opposite override direction).Generated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Claiming this issue (shared root cause: npm_non_registry_entries ignores overrides that turn a registry spec into a URL/git/file spec). Branch: agent/fix-npm-override-to-url. Claim-ID: 2026-11-02T02:21:26Z-b6ded9
Generated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Draft PR: #1966
Generated by Claude Code
Test npm overrides that swap in a URL spec
Skip npm copies an override sends to a URL
Keep URL-overridden npm copies unwindable
E2E: hosted npm skips a URL-overridden copy
mikolalysenko commentedon Nov 2, 2026
[agent] #1966 is blocked on a maintainer decision. An override scoped to a logical ancestor that is not on the copy's physical
node_modules/path (e.g.{"a": {"left-pad": <url>}}withbandleft-padhoisted) is still treated as a registry install, so that copy is still rewired and attested. The options are listed in #1966 (comment):Reply on the PR with the choice and the next run will implement it.
Generated by Claude Code