Skip to content

vlt hosted and vendored vex attests not_affected while a bundled copy of the same name@version in node_modules/.vlt stays unpatched (the #325 fix covers npm locks only) #471

Description

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

Summary

In a vlt project, the patched name@version is often also shipped as a bundled copy inside another package's tarball (bundleDependencies). vlt unpacks that copy into the parent's store entry, node_modules/.vlt/~npm~<parent>@<ver>/node_modules/<parent>/node_modules/<name>. vlt-lock.json has no node for it. The hosted and vendored rewiring only reaches the lock node, so after vlt ci the bundled copy is still the registry bytes. Even so, scan reports success with no warning, and vex attests the package not_affected / inline_mitigations_already_exist.

For npm this exact case was fixed in #337 (#325). The hosted rewriter warns redirect_npm_bundled_instance_skipped, and VEX drops the ref with patched_ref_unattributable. Neither guard exists for vlt, because vlt's lock doesn't record bundled copies, and vex/discover/vlt.rs and patch/redirect/vlt.rs only look at the lock.

Impact

The VEX document certifies a vulnerability as mitigated while the build ships and runs unpatched bytes of the vulnerable version (whatever code requires it through the bundling parent). This is the same false attestation #325 fixed for npm, and it reproduces on every OS and every vlt version tested.

Repro

Mock registry and patch server: the mock from the ledger's probe workflow (left-pad@1.3.0 pristine/patched, plus a bundler@1.0.0 whose tarball has bundleDependencies: ["left-pad"] and package/node_modules/left-pad/ with pristine bytes). The registry runs on :18555 and the patch API on :18556.

export SOCKET_NPM_REGISTRY=http://127.0.0.1:18555 SOCKET_API_URL=http://127.0.0.1:18556 \
       SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18556 SOCKET_ORG_SLUG=test-org SOCKET_API_TOKEN=fake
mkdir proj && cd proj
echo '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0","bundler":"1.0.0"}}' > package.json
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"}}}' > vlt.json
vlt install
socket-patch scan --yes --json            # or --mode vendored
rm -rf node_modules && vlt ci
node -p "require('left-pad')"              # patched
node -p "require('bundler')"               # pristine  <- bundled copy, unpatched
cat node_modules/.vlt/~npm~bundler@1.0.0/node_modules/bundler/node_modules/left-pad/index.js   # 'pristine'
socket-patch vex --output vex.json        # rc 0, no warning
# statement: not_affected / inline_mitigations_already_exist, subcomponent pkg:npm/left-pad@1.3.0

The lock after vlt install has only ~npm~bundler@1.0.0 and ~npm~left-pad@1.3.0, with no trace of the bundled copy.

Same project with npm (npm install → scan → vex): the scan warns redirect_npm_bundled_instance_skipped ("that copy stays UNPATCHED"), and vex prints "…also installs a bundled copy of it at node_modules/bundler/node_modules/left-pad … the patch is not attested". No statement is emitted.

Related symptom: if the patched package is present only as a bundled copy (dependencies: {"bundler":"1.0.0"}), hosted scan exits 0 with redirect_vlt_entry_not_found "vlt-lock.json has no default-registry entry for left-pad@1.3.0; run vlt install first". That's misleading: vlt install can't help, because the copy is bundled. npm's equivalent says the bundled copy cannot be redirected.

Expected vs actual

  • Expected (CLI_CONTRACT.md, "Contested locks"): "A bundled npm copy … of the same name@version contests the reference too, … in any other lock. npm unpacks it from the parent package's tarball, so no rewire reaches it and it stays unpatched." vlt unpacks bundled deps the same way. So for vlt, the ref should be dropped with patched_ref_unattributable, and scan should warn that the bundled copy stays unpatched, the way it does for npm.
  • Actual: scan rc 0 success with no bundled warning, and vex rc 0 with a not_affected statement.

OS × vlt version (main 5678b76)

OS vlt 1.0.10 vlt 1.2.0 vlt 1.3.3
Linux (ubuntu-latest + sandbox) repro (hosted + vendored) repro (hosted + vendored) repro (hosted + vendored; sandbox 3/3 runs)
macOS (macos-latest) repro (hosted + vendored) repro (hosted + vendored) repro (hosted + vendored)
Windows (windows-latest) repro (hosted + vendored) repro (hosted + vendored) repro (hosted + vendored)

All 18 cells print RESULT top=patched bundled=pristine and STATEMENT not_affected inline_mitigations_already_exist pkg:npm/left-pad@1.3.0. Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36871535059

First bad: not a regression. It has been present since vlt hosted/vendored + VEX support; release 4.0.0 predates vlt support. #337 fixed npm only.

Suspect code

  • crates/socket-patch-core/src/vex/discover/vlt.rs:93 (extract): attests from lock nodes only, with no bundled-copy contest like vex/discover/npm.rs:95. Since the vlt lock omits bundled copies, the contest has to come from the installed store (any node_modules/.vlt/*/node_modules/<parent>/node_modules/<name> whose package.json is the ref's name@version) or from each lock node's manifest bundleDependencies.
  • crates/socket-patch-core/src/patch/redirect/vlt.rs and vendor/vlt_lock.rs:735: vendored only refuses when the target itself declares bundleDependencies (vendor_bundled_deps_unsupported). It doesn't check whether some other package bundles the target. Hosted has no counterpart of redirect_npm_bundled_instance_skipped (patch/redirect/mod.rs:904).

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