Skip to content

Vendored npm vex and scan --mode vendored --vex attest not_affected beside a file: directory copy of the patched name@version, which npm installs unpatched #1511

Closed

Description

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

Summary

This is the vendored-mode counterpart of #1481. In an npm workspace under install-strategy=nested, member a depends on registry left-pad@1.3.0 and member b depends on left-pad through a file: directory holding the same release ("left-pad": "file:../../lpdir"). The lock then has a registry entry packages/a/node_modules/left-pad and a versionless link entry packages/b/node_modules/left-pad ("resolved": "lpdir", "link": true).

scan --mode vendored --vex vendors a's copy and attests not_affected. A standalone vex after npm ci attests not_affected too, and vendor --check passes. But b's left-pad is a symlink to lpdir, which no rewrite reaches, so it stays unpatched.

The same shape with a file: tarball or a URL copy is handled correctly in vendored mode: patched_ref_unattributable, nothing attested. On main, hosted mode refuses all three shapes, the directory one included (#1482).

Impact

The VEX says the app isn't affected while one workspace member still loads the vulnerable bytes. Nothing in the run says so: the only output is vendored_tree_out_of_sync, and its remedy ("re-run your package manager's install") can't fix this. The scan reports the link copy as vendor_workspace_member_skipped, although it's a file: dependency, not a workspace member.

Repro

You need a mock of the org patch API that serves a patch for left-pad@1.3.0 (batch / view / blob / patches/package routes). Real npm installs.

mkdir -p app/packages/a app/packages/b && cd app
echo '{"name":"app","version":"1.0.0","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../../lpdir"}}' > packages/b/package.json
mkdir lpdir && npm pack left-pad@1.3.0 && tar xzf left-pad-1.3.0.tgz -C lpdir --strip-components=1
echo install-strategy=nested > .npmrc
npm install
socket-patch scan --mode vendored --vex vex.json --json --api-url $MOCK --org o --api-token fake --patch-server-url $MOCK
# exit 0, vex.json: not_affected
rm -rf node_modules packages/*/node_modules && npm ci
grep -c SOCKET-PATCHED packages/a/node_modules/left-pad/index.js   # 1
grep -c SOCKET-PATCHED lpdir/index.js                              # 0  (= packages/b/node_modules/left-pad)
socket-patch vex --output vex2.json    # exit 0, not_affected (warning vendored_tree_out_of_sync)
socket-patch vendor --check            # exit 0, "committed artifact and wiring verified"

Expected vs actual

  • Expected: CLI_CONTRACT.md, "Contested locks": a reference withheld because an unreachable copy installs beside it ("an npm copy … one npm installs from a git / url / file: spec") "is never attested". Vendored mode should drop the ref with patched_ref_unattributable, as it already does for the file: tarball and URL shapes and as hosted mode does since Fix npm file: copy attested by in-run VEX (#1481) #1482.
  • Actual: exit 0 and not_affected from both the in-run and the standalone vex. vendor --check exits 0.

Matrix (Linux, Node 22.22.0; npm 12 on Node 24)

npm layout copy shape in-run --vex standalone vex after npm ci
10.9.4 nested file: dir attests (twice) attests
11.6.2 nested file: dir attests attests
12.2.0 nested file: dir attests attests
10.9.4 nested file: tarball refused (pass) —
10.9.4 nested URL tarball refused (pass) —
8.19.4 / 10.9.4 / 12.2.0 hoisted (default) file: dir npm dedupes onto the link, nothing vendored (pass) exit 2 (pass)
10.9.4 (hosted mode, for comparison) nested file: dir refused (pass, #1482) refused (pass)

First bad version

Present in published v5.0.1 and on main 887a5f7. v4.0.0 can't run this flow against the same mock, so I didn't bisect further. This looks like a gap left by #1482 rather than a regression.

Suspect code

crates/socket-patch-core/src/vex/discover/npm.rs:472-478 (drop_non_registry_installs): an entry with no version is skipped (continue), so the versionless link entry that npm records for a file: directory dependency never contests the ref. #1482 taught the hosted rewriter to resolve that identity through the link target (npm_link_source_identity, crates/socket-patch-core/src/patch/redirect/mod.rs:1412), but discovery, which vendored vex / vendor --check rely on, has no equivalent. The vendored rewriter also labels the copy vendor_workspace_member_skipped.

No probe runs: probe branches are still blocked for this routine (see ledger #302), so this was tested on Linux only.

Activity

  1. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: npm lock discovery skips a versionless file: directory link entry instead of resolving its identity through the link target, as the hosted rewriter does since #1482). Branch: agent/fix-npm-vendored-file-dir-copy. Claim-ID: 2026-10-14T00:20:52Z-bdc2d7


    Generated by Claude Code

  2. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1513


    Generated by Claude Code

  3. added 2 commits that reference this issue on Oct 14, 2026
    5bb379e
    7b7f58c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions