Skip to content

Agent-mode apply writes through node_modules links into first-party source (npm workspace members, file: deps, npm link targets), overwriting the user's code, and rollback restores upstream bytes instead #626

Description

[agent] Found by the scheduled npm bug-hunt routine (ledger #302).

Summary

Agent-mode scan / apply follows a node_modules/<name> symlink into first-party source — an npm workspace member, a file: directory dependency, or an npm link target — whenever that local package's name@version matches a patched registry package. The local index.js doesn't match the patch's beforeHash, so the default mismatch policy overwrites the user's own source with the upstream patched file (only a warning), vex then attests not_affected, and rollback "restores" the upstream original file instead of the user's code. The user's source is gone in every case.

Vendored mode already recognizes this case and refuses (vendor_workspace_member: "is a workspace member of this project; patch the source directly instead of vendoring it"), and hosted mode correctly leaves the link: true lock entry alone (redirect_npm_entry_not_found). Only agent mode writes through the link.

Impact

  • Data loss in committed first-party code: a monorepo that keeps a fork of a dependency as a workspace (same name@version as upstream — a common way to carry a local fix) has the fork's files silently replaced by upstream content on socket-patch scan. rollback doesn't undo it.
  • Writes outside the project: with npm link left-pad, the link target is the developer's checkout of left-pad in another directory, and agent mode rewrites files there. docs/ecosystems.md uses the same reasoning to skip pnpm's global virtual store ("other projects on the machine load the same files, so patching it in place would patch them as well").
  • Misleading VEX: the attestation covers code that isn't the registry package the advisory is about.

Repro (real npm, local mock patch API)

# mock API: a patch for left-pad@1.3.0 that prepends a marker to index.js
mkdir -p ws/packages/left-pad && cd ws && git init -q .
echo '{"name":"left-pad","version":"1.3.0","main":"index.js"}' > packages/left-pad/package.json
echo 'module.exports = "first-party fork";' > packages/left-pad/index.js
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*"]}' > package.json
# or: "dependencies":{"left-pad":"file:packages/left-pad"}   (same result)
npm install && git add -A && git commit -qm init
ls -l node_modules/left-pad            # -> ../packages/left-pad (link: true in package-lock.json)

socket-patch scan --mode agent --api-url $MOCK --patch-server-url $MOCK --org o --api-token x
#   Warning: pkg:npm/left-pad@1.3.0 package/index.js did not match the patch's expected original
#   content; applied the full verified patched content instead (pass --strict to fail on mismatches)
#   Summary: 1 of 1 targeted patch applied          (exit 0)
git diff --stat -- packages            # packages/left-pad/index.js | 54 +++++-  (fork replaced by upstream)
socket-patch vex ...                   # exit 0, not_affected
socket-patch rollback ...              # exit 0
head -c 60 packages/left-pad/index.js  # "/* This program is free software…"  — upstream original, not the fork

npm link variant: cd ~/dev/left-pad && npm link; cd proj && npm link left-pad; socket-patch scan --mode agent → ~/dev/left-pad/index.js is rewritten.

Expected vs actual

  • Expected: a node_modules entry that is a link to a workspace member / file: directory / npm link target (the lock marks it "link": true) isn't an installed copy of the registry package, so agent mode skips it with a diagnostic, as vendored mode already does (vendor_workspace_member), and never writes through it. The default mismatch overwrite (crates/socket-patch-cli/CLI_CONTRACT.md, "mismatch-policy note"; apply.rs:45: "What tolerance can do is discard local modifications to the dependency file") is justified for an installed dependency that npm ci can restore. It isn't justified for first-party source that no reinstall brings back. docs/ecosystems.md also refuses to patch a store outside the project in place, which is the same hazard as an npm link target.
  • Actual: agent mode patches the link target (overwriting it on a hash mismatch), reports success, VEX attests it, and rollback writes the upstream original over the user's file.

Matrix (main 045d7ec, Node 22 / 24)

OS npm workspace member file: dir dep npm link
Linux 6.14.18 – (npm 6 has no workspaces) repro –
Linux 8.19.4 repro repro –
Linux 10.9.4 repro (2/2) repro repro
Linux 12.2.0 repro repro –
macOS (macos-latest, arm64) 10.9.7 repro (2/2, second run) repro –
Windows (latest) 8.19.4 / 10.9.7 / 12.2.0 repro (junction) repro –
Ubuntu (Actions) 8.19.4 / 10.9.7 / 12.2.0 repro repro –

--strict blocks it (exit 1, nothing written). Hosted: no rewrite, loud redirect_npm_entry_not_found. Vendored: refused, vendor_workspace_member.

First bad version: not a regression. Released v4.0.0 (apply against the same manifest) overwrites the fork the same way.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1933 acceptable_package_entry: importer trees accept file_type.is_symlink() for any entry (meant for pnpm/vlt store links, see the doc comment at :1772, which also lists "npm link targets"), with no check of where the link resolves to: into a store under node_modules, or into first-party source / outside the project.
  • Vendored's equivalent guard: crates/socket-patch-core/src/vendor/npm_lock.rs:459 (vendor_workspace_member).
  • Combined with the default MismatchPolicy::Warn (crates/socket-patch-core/src/patch/apply.rs:51), which overwrites on a mismatch.

Probe runs: https://github.com/SocketDev/socket-patch/actions/runs/37081541007 (ubuntu / macOS / Windows × npm 8 / 10 / 12; the first macOS jobs lost a startup race with the mock server, so scan exited 1 on a connect timeout) and https://github.com/SocketDev/socket-patch/actions/runs/37082844719 (macOS rerun with the server warmed: reproduces).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions