[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
npm 12.1 added native patching (npm patch add / npm patch commit). It records patchedDependencies in the root package.json, stores unified diffs under patches/, writes "lockfileVersion": 4 and adds a patched: {integrity, path} object to the lock entry. On every install, npm extracts the locked tarball and then applies the user's diff, and any apply failure is a hard error (EPATCHFAILED).
socket-patch doesn't know about any of this. rg patchedDependencies crates/ docs/ returns nothing.
- Hosted (the v5 default):
scan rewrites the v4 lock entry's resolved / integrity to the hosted tarball and keeps the patched object. It exits 0 with no warning. When the user's diff touches the lines the Socket patch changes, every later npm ci and npm install fails EPATCHFAILED, so the project can't install at all until someone runs rollback or reverts the lock. That's plausible in practice: a team hand-patched the vulnerable line with npm patch, then adopted Socket for the same advisory.
- Hosted, non-overlapping diff:
npm ci installs both patches. vex then refuses: omitting pkg:npm/left-pad@1.3.0 from VEX: a patched file matches neither the original nor the patched content (hash_mismatch), exit 1. The hosted patch is installed but can never be attested. This fails closed, but nothing tells the user why.
- Vendored: refuses with
package-lock.json has lockfileVersion Some(4); only v2/v3 locks … — run \npm install` with npm >= 7 to upgrade it(exit 1). Refusing is reasonable, but the advice is wrong: npm 12 wrote that v4 lock, and re-runningnpm install` keeps it at v4. Hosted, for its part, rewrites the same v4 lock without any version gate.
- Agent:
apply overwrites the user's npm-patched file with the full Socket content (did not match the patch's expected original content; applied the full verified patched content instead), discarding the user's local patch until the next install re-applies it. That's the documented non---strict fallback, but the cause isn't named.
Impact
A successful socket-patch scan (exit 0, "Switched 1 package to hosted patches") breaks every subsequent npm install in CI. The run gives no hint, before or after, that patchedDependencies is involved.
Repro (Linux, npm 12.2.0 on Node 24.21; also npm 12.1.0)
The patch API is a local mock on --api-url / --patch-server-url. The Socket patch for left-pad@1.3.0 changes return pad + str; to return pad + String(str); /* SOCKET-PATCHED */.
mkdir p && cd p
echo '{"name":"c","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
npm install
ed=$(npm patch add left-pad | grep -o '/tmp/npm-patch/[^ ]*' | tail -1)
sed -i 's/return pad + str;/return pad + (str + ""); \/\/ user-local-patch/' "$ed/index.js"
npm patch commit "$ed" # package.json gains patchedDependencies, lock becomes lockfileVersion 4
git init -q && printf 'node_modules\n' > .gitignore && git add -A && git commit -qm base
socket-patch scan --yes # hosted; exit 0, no warning
git add -A && git commit -qm scan
git clone -q . ../f && cd ../f
npm ci # npm error code EPATCHFAILED / patch could not be applied to index.js (exit 1)
npm install # same EPATCHFAILED (exit 1)
The lock entry after the scan:
{"version":"1.3.0","resolved":"http://127.0.0.1:8766/a/<uuid>/left-pad-1.3.0.tgz","integrity":"sha512-Rds5…","patched":{"integrity":"sha512-ut09…","path":"patches/left-pad@1.3.0.patch"}}
socket-patch rollback --yes restores the lock byte-exact and installs work again.
Expected vs actual
- Expected: hosted mode pins only entries npm will install as the hosted artifact. The redirect rules already refuse or warn whenever an entry can't be safely pinned (
redirect_npm_non_registry_entry_skipped, redirect_npm_legacy_client, …; see CLI_CONTRACT.md "npm-family flavor coverage"). An entry carrying npm's patched object (or a patchedDependencies key for that name@version) should be refused or at least warned about, never reported as a clean switch. The vendored refusal should name patchedDependencies / lockfileVersion 4 instead of telling the user to upgrade with npm >= 7.
- Actual: hosted exits 0 and the next install fails EPATCHFAILED. Vendored gives misleading advice.
vex refuses with a generic hash_mismatch.
Matrix (Linux sandbox; this is OS-independent lock logic)
| npm |
user diff overlaps the Socket patch |
hosted scan |
fresh npm ci |
vex |
| 12.2.0 / Node 24.21 |
yes |
exit 0, lock rewritten |
EPATCHFAILED (also npm install) |
n/a |
| 12.2.0 / Node 24.21 |
no (diff appends at EOF, or edits line 1) |
exit 0 |
both patches installed |
exit 1 hash_mismatch |
| 12.1.0 / Node 24.21 |
yes |
exit 0 |
EPATCHFAILED |
n/a |
| 12.2.0 vendored |
either |
exit 1, lockfileVersion Some(4) "use npm >= 7" |
— |
— |
| ≤ 11 |
— |
npm ≤ 11 has no npm patch; not affected |
|
|
Reproduced twice on main, 045d7ec.
First bad version
It isn't a regression: v4.0.0 (scan --mode hosted) rewrites the same v4 lock the same way, and with allow-remote=all the fresh npm ci fails EPATCHFAILED. The trigger is npm 12.1.0's new feature.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:797 rewrite_npm_lock / :1062 rewrite_npm_entry: no lockfileVersion gate beyond the v1 warning, and no check for the entry's patched object or the root patchedDependencies.
crates/socket-patch-core/src/vendor/npm_lock.rs:422 lock_version_gate: the v4 refusal text.
- docs/testing/npm-compatibility.md has no lockfileVersion 4 /
npm patch row.
No probe runs: the logic doesn't depend on the OS.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
npm 12.1 added native patching (
npm patch add/npm patch commit). It recordspatchedDependenciesin the rootpackage.json, stores unified diffs underpatches/, writes"lockfileVersion": 4and adds apatched: {integrity, path}object to the lock entry. On every install, npm extracts the locked tarball and then applies the user's diff, and any apply failure is a hard error (EPATCHFAILED).socket-patch doesn't know about any of this.
rg patchedDependencies crates/ docs/returns nothing.scanrewrites the v4 lock entry'sresolved/integrityto the hosted tarball and keeps thepatchedobject. It exits 0 with no warning. When the user's diff touches the lines the Socket patch changes, every laternpm ciandnpm installfailsEPATCHFAILED, so the project can't install at all until someone runsrollbackor reverts the lock. That's plausible in practice: a team hand-patched the vulnerable line withnpm patch, then adopted Socket for the same advisory.npm ciinstalls both patches.vexthen refuses:omitting pkg:npm/left-pad@1.3.0 from VEX: a patched file matches neither the original nor the patched content (hash_mismatch), exit 1. The hosted patch is installed but can never be attested. This fails closed, but nothing tells the user why.package-lock.json has lockfileVersion Some(4); only v2/v3 locks … — run \npm install` with npm >= 7 to upgrade it(exit 1). Refusing is reasonable, but the advice is wrong: npm 12 wrote that v4 lock, and re-runningnpm install` keeps it at v4. Hosted, for its part, rewrites the same v4 lock without any version gate.applyoverwrites the user's npm-patched file with the full Socket content (did not match the patch's expected original content; applied the full verified patched content instead), discarding the user's local patch until the next install re-applies it. That's the documented non---strictfallback, but the cause isn't named.Impact
A successful
socket-patch scan(exit 0, "Switched 1 package to hosted patches") breaks every subsequent npm install in CI. The run gives no hint, before or after, thatpatchedDependenciesis involved.Repro (Linux, npm 12.2.0 on Node 24.21; also npm 12.1.0)
The patch API is a local mock on
--api-url/--patch-server-url. The Socket patch forleft-pad@1.3.0changesreturn pad + str;toreturn pad + String(str); /* SOCKET-PATCHED */.The lock entry after the scan:
{"version":"1.3.0","resolved":"http://127.0.0.1:8766/a/<uuid>/left-pad-1.3.0.tgz","integrity":"sha512-Rds5…","patched":{"integrity":"sha512-ut09…","path":"patches/left-pad@1.3.0.patch"}}socket-patch rollback --yesrestores the lock byte-exact and installs work again.Expected vs actual
redirect_npm_non_registry_entry_skipped,redirect_npm_legacy_client, …; see CLI_CONTRACT.md "npm-family flavor coverage"). An entry carrying npm'spatchedobject (or apatchedDependencieskey for that name@version) should be refused or at least warned about, never reported as a clean switch. The vendored refusal should namepatchedDependencies/ lockfileVersion 4 instead of telling the user to upgrade with npm >= 7.vexrefuses with a generichash_mismatch.Matrix (Linux sandbox; this is OS-independent lock logic)
npm civexnpm install)hash_mismatchlockfileVersion Some(4)"use npm >= 7"npm patch; not affectedReproduced twice on main,
045d7ec.First bad version
It isn't a regression: v4.0.0 (
scan --mode hosted) rewrites the same v4 lock the same way, and withallow-remote=allthe freshnpm cifails EPATCHFAILED. The trigger is npm 12.1.0's new feature.Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:797rewrite_npm_lock/:1062rewrite_npm_entry: no lockfileVersion gate beyond the v1 warning, and no check for the entry'spatchedobject or the rootpatchedDependencies.crates/socket-patch-core/src/vendor/npm_lock.rs:422lock_version_gate: the v4 refusal text.npm patchrow.No probe runs: the logic doesn't depend on the OS.