[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
The #326 fix (#345, 6e7ef74) decides that an npm lock entry is "not installed from the registry" when any dependent's raw spec for that name is a git, URL or file: spec (npm_non_registry_entries, crates/socket-patch-core/src/vendor/npm_origin.rs:71-99). It doesn't take root overrides into account. When a transitive dependency declares left-pad: "github:…" and the project overrides it with "overrides": {"left-pad": "1.3.0"}, npm installs left-pad@1.3.0 from the registry. The lock records that: resolved is https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz, with registry integrity. The dependent's dependencies entry still shows the git spec, because npm copies it from the dependent's package.json.
socket-patch on main then:
- hosted (
scan, the v5 default): skips the entry with redirect_npm_non_registry_entry_skipped ("npm installs it from that spec, so that copy stays UNPATCHED; depend on the registry release to patch it"), redirects 0 packages and exits 0 with status: success;
- vendored (
scan --mode vendored): fails with vendor_lock_entry_not_rewritable, exit 1;
- vex: refuses to attest the package even when the lock was correctly redirected (by v4.0.0) and the patched bytes are installed (
no_applicable_patches, exit 1).
Overriding a git dependency with the registry release is exactly what the warning tells users to do, yet the warning still fires afterwards.
Impact
A project that pins a git-sourced transitive dependency back to the registry through overrides can't get hosted or vendored patches for that package at all. v4.0.0 redirected it correctly, and npm ci installed the patched bytes. The hosted path exits 0, so a CI job that runs socket-patch scan reports success while the vulnerable package stays unpatched. The only signal is a warning whose diagnosis is wrong.
Repro (Linux, npm 10.9.4 / Node 22; same on npm 8, 11, 12)
mkdir -p pkga app
echo '{"name":"pkga","version":"1.0.0","dependencies":{"left-pad":"github:stevemao/left-pad#v1.3.0"}}' > pkga/package.json
echo 'module.exports=require("left-pad")' > pkga/index.js
(cd pkga && npm pack) && mv pkga/pkga-1.0.0.tgz app/ && cd app
echo '{"name":"app","version":"0.0.0","private":true,"dependencies":{"pkga":"file:pkga-1.0.0.tgz"},"overrides":{"left-pad":"1.3.0"}}' > package.json
npm install
# lock: node_modules/left-pad resolved=https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz
# node_modules/pkga dependencies.left-pad = "github:stevemao/left-pad#v1.3.0"
socket-patch scan --json --yes # with a free patch for pkg:npm/left-pad@1.3.0
The patch source was a local mock of the patch API (--api-url / --patch-server-url pointed at it) serving one free patch for pkg:npm/left-pad@1.3.0.
Actual on main 61cfb9b:
"status": "success", exit 0
"redirect": {"mode":"hosted","redirected":0,"rewrittenFiles":[],
"warnings":[{"code":"redirect_npm_non_registry_entry_skipped",
"detail":"lock entry `node_modules/left-pad` is not installed from the registry (`node_modules/pkga` depends on it as \"github:stevemao/left-pad#v1.3.0\", which npm installs from that spec) and CANNOT be redirected — npm installs it from that spec, so that copy stays UNPATCHED; ..."}]}
package-lock.json is unchanged, and after rm -rf node_modules && npm ci, node_modules/left-pad/index.js is the original. scan --mode vendored: partialFailure, exit 1, vendor_lock_entry_not_rewritable with the same reason.
v4.0.0 (scan --mode hosted) on the same project: redirected: 1, the lock pin is rewritten, and a fresh npm ci installs the patched index.js. v4.0.0's vex attests not_affected. Main's vex on that v4-wired, correctly patched tree returns no_applicable_patches (exit 1).
Expected vs actual
Matrix (Linux sandbox, real npm installs)
| npm |
lockfileVersion |
main 61cfb9b hosted |
main vendored |
v4.0.0 hosted |
| 8.19.4 |
2 |
fail (skipped, exit 0) |
— |
— |
| 10.9.4 |
3 |
fail (skipped, exit 0) |
— |
pass (npm ci installs patched bytes) |
| 11.21.0 |
3 |
fail (skipped, exit 0) |
fail (vendor_lock_entry_not_rewritable, exit 1) |
— |
| 12.2.0 (Node 24.21) |
3 |
fail (skipped, exit 0) |
— |
— |
macOS and Windows weren't probed; the classification is pure lock-JSON logic with no OS-specific paths.
First bad
#345 (6e7ef74, "Fix npm rewiring git/URL/file lock entries (#326)"). v4.0.0 predates it and works.
Suspect code
crates/socket-patch-core/src/vendor/npm_origin.rs:71-99: the edge loop marks the resolved target non-registry whenever !npm_spec_is_registry(spec), without checking the target entry's own resolved (a registry tarball URL here) or the root overrides. One option: when the edge's target entry has a registry resolved + integrity (not git/file:) and the root package.json overrides cover the name, treat it as a registry copy. Or more simply, trust a registry tarball resolved over a non-registry edge spec only when an override applies, since #326 showed that resolved alone isn't authoritative without one.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
The #326 fix (#345,
6e7ef74) decides that an npm lock entry is "not installed from the registry" when any dependent's raw spec for that name is a git, URL orfile:spec (npm_non_registry_entries,crates/socket-patch-core/src/vendor/npm_origin.rs:71-99). It doesn't take rootoverridesinto account. When a transitive dependency declaresleft-pad: "github:…"and the project overrides it with"overrides": {"left-pad": "1.3.0"}, npm installsleft-pad@1.3.0from the registry. The lock records that:resolvedishttps://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz, with registry integrity. The dependent'sdependenciesentry still shows the git spec, because npm copies it from the dependent's package.json.socket-patch on main then:
scan, the v5 default): skips the entry withredirect_npm_non_registry_entry_skipped("npm installs it from that spec, so that copy stays UNPATCHED; depend on the registry release to patch it"), redirects 0 packages and exits 0 withstatus: success;scan --mode vendored): fails withvendor_lock_entry_not_rewritable, exit 1;no_applicable_patches, exit 1).Overriding a git dependency with the registry release is exactly what the warning tells users to do, yet the warning still fires afterwards.
Impact
A project that pins a git-sourced transitive dependency back to the registry through
overridescan't get hosted or vendored patches for that package at all. v4.0.0 redirected it correctly, andnpm ciinstalled the patched bytes. The hosted path exits 0, so a CI job that runssocket-patch scanreports success while the vulnerable package stays unpatched. The only signal is a warning whose diagnosis is wrong.Repro (Linux, npm 10.9.4 / Node 22; same on npm 8, 11, 12)
The patch source was a local mock of the patch API (
--api-url/--patch-server-urlpointed at it) serving one free patch forpkg:npm/left-pad@1.3.0.Actual on main
61cfb9b:package-lock.jsonis unchanged, and afterrm -rf node_modules && npm ci,node_modules/left-pad/index.jsis the original.scan --mode vendored:partialFailure, exit 1,vendor_lock_entry_not_rewritablewith the same reason.v4.0.0 (
scan --mode hosted) on the same project:redirected: 1, the lock pin is rewritten, and a freshnpm ciinstalls the patchedindex.js. v4.0.0'svexattestsnot_affected. Main'svexon that v4-wired, correctly patched tree returnsno_applicable_patches(exit 1).Expected vs actual
github:user/repo,https://…/x.tgz,file:…)". With an override in place, npm installs from the override's registry spec, which the lock'sresolved/integrityconfirm. The entry is a registry copy and should be redirected or vendored and attested, as v4.0.0 did.Matrix (Linux sandbox, real npm installs)
61cfb9bhostednpm ciinstalls patched bytes)vendor_lock_entry_not_rewritable, exit 1)macOS and Windows weren't probed; the classification is pure lock-JSON logic with no OS-specific paths.
First bad
#345 (
6e7ef74, "Fix npm rewiring git/URL/file lock entries (#326)"). v4.0.0 predates it and works.Suspect code
crates/socket-patch-core/src/vendor/npm_origin.rs:71-99: the edge loop marks the resolved target non-registry whenever!npm_spec_is_registry(spec), without checking the target entry's ownresolved(a registry tarball URL here) or the rootoverrides. One option: when the edge's target entry has a registryresolved+integrity(not git/file:) and the root package.jsonoverridescover the name, treat it as a registry copy. Or more simply, trust a registry tarballresolvedover a non-registry edge spec only when an override applies, since #326 showed thatresolvedalone isn't authoritative without one.