Skip to content

Agent-mode npm apply, apply --check and vex skip a declared workspace member whose directory is named build, dist, vendor, tmp, temp or coverage, so its nested copy stays unpatched while VEX attests not_affected #1627

Open

Description

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

Summary

The npm crawler's workspace walk prunes every directory named dist, build, coverage, tmp, temp, __pycache__ or vendor (SKIP_DIRS). It does this even when the root package.json workspaces field declares that directory as a member, and npm installs it from the root lock. When npm can't hoist a member's dependency (a version conflict with the root), npm installs it into <member>/node_modules/. A member at packages/build, packages/dist, tools/build or vendor/lib is common in real monorepos, and its nested copy is then never crawled. As a result:

  • apply patches the other copies and prints 1 of 1 targeted patch applied, 0 … not found on disk. The member's copy stays unpatched with no warning.
  • apply --check reports Patches are in sync.
  • vex attests not_affected / inline_mitigations_already_exist for the product.
  • At runtime, Node resolves the member's own unpatched copy (require.resolve('ms') from the member → <member>/node_modules/ms/index.js).

Impact

The tool reports a false security attestation (VEX not_affected) and a passing CI audit (apply --check), while a vulnerable copy that npm installed from the project's own lock runs in production code. Nothing warns about it.

Repro (Linux, npm 10.9.4 / Node 22; same on npm 8.19.4 and npm 12.2.1 / Node 24)

mkdir ws && cd ws && mkdir -p packages/app packages/build
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*"],"dependencies":{"ms":"2.1.3"}}' > package.json
echo '{"name":"app","version":"1.0.0","dependencies":{"ms":"2.0.0"}}'   > packages/app/package.json
echo '{"name":"build","version":"1.0.0","dependencies":{"ms":"2.0.0"}}' > packages/build/package.json
npm install
# -> node_modules/ms@2.1.3, packages/app/node_modules/ms@2.0.0, packages/build/node_modules/ms@2.0.0
# Stage a .socket/manifest.json + blobs for pkg:npm/ms@2.0.0 that appends a marker to index.js
# (a hand-staged agent manifest; any real ms@2.0.0 patch behaves the same)
socket-patch apply
#   Summary: 1 of 1 targeted patch applied, 0 already patched, 0 not found on disk
grep -c socket-patched packages/app/node_modules/ms/index.js    # 1  (patched)
grep -c socket-patched packages/build/node_modules/ms/index.js  # 0  (UNPATCHED)
socket-patch apply --check          # Patches are in sync (1 patch checked).  exit 0
socket-patch vex --product pkg:npm/root@1.0.0 -O vex.json   # statement: not_affected
(cd packages/build && node -p "require.resolve('ms')")       # …/packages/build/node_modules/ms/index.js (unpatched)

If you rename packages/build to packages/lib, both copies are patched (control).

Expected vs actual

  • Expected: CLI_CONTRACT (apply --check) says "each installed copy hashes to the record's afterHash", and drift is exit 1. docs/ecosystems.md ("npm: which node_modules trees are crawled") says the scan collects "every node_modules found in the directories below it (workspace members, nested projects)". The name-based prune is meant for build-output / cache dirs ("well-known non-workspace dirs" in the code comment), not for a directory that workspaces declares and that npm installs into from the root lock. At a minimum, the crawler should never let vex attest not_affected while such a copy is unpatched.
  • Actual: declared members whose directory names are in SKIP_DIRS are skipped silently by apply, apply --check, agent scan and vex.

Matrix (agent mode, main 657fce2)

OS npm / Node member dir apply patches member copy apply --check vex
Linux 10.9.4 / 22.22.0 packages/build no in sync (wrong) not_affected (wrong)
Linux 10.9.4 / 22.22.0 packages/dist no in sync (wrong) not_affected (wrong)
Linux 10.9.4 / 22.22.0 vendor/lib no in sync (wrong) not_affected (wrong)
Linux 10.9.4 / 22.22.0 tools/build no in sync (wrong) not_affected (wrong)
Linux 8.19.4 / 22.22.0 packages/build no in sync (wrong) not_affected (wrong)
Linux 12.2.1 / 24.22.0 packages/build no in sync (wrong) not_affected (wrong)
Linux 10.9.4 packages/lib (control) yes in sync not_affected (correct)

Release v5.0.1 (npm @socketsecurity/socket-patch) behaves the same, so this is not a regression since the last release. Not tested this run: hosted and vendored modes, which rewire from the lock rather than the crawl; macOS / Windows (probe branches are currently blocked for this routine, but the walk is OS-independent).

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:21 (SKIP_DIRS)
  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:2965 (workspace_children, which applies SKIP_DIRS without consulting the root package.json workspaces globs; the crate already expands those globs for Bun in bun_workspace_pattern_members_sync, around line 768)

The crawler is shared by the other JS package managers, so pnpm / yarn / Bun workspaces with a member at such a path probably behave the same. This report is scoped to npm.

Activity

  1. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (npm family). This is a new defect, not a duplicate. No open or merged PR addresses it. It is confirmed in the code: workspace_children in crates/socket-patch-core/src/crawlers/npm_crawler.rs drops SKIP_DIRS names before it checks the root workspaces globs.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the npm-family workspace walk prunes SKIP_DIRS names even for declared workspaces members). Branch: agent/fix-npm-skipdir-workspace-members. Claim-ID: 20261017T122110Z-c7f3f6


    Generated by Claude Code

  3. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #1628


    Generated by Claude Code

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