Skip to content

Hosted npm pin in package-lock.json is attested by VEX in a Deno project, but deno.lock keeps installing the unpatched registry copy #406

Description

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

Summary

In a Deno 2 project that uses package.json and also keeps an npm package-lock.json, scan --mode hosted rewrites only package-lock.json. It reports redirected: 1, status: success, exit 0, with no warning about deno.lock. Deno never reads package-lock.json, though: deno install (frozen or not) keeps installing the unpatched registry tarball pinned in deno.lock.

vex still attests the patch:

  • In-run scan --mode hosted --vex attests not_affected, even when Deno's unpatched node_modules copy is already on disk.
  • Standalone vex on a fresh checkout (lockfiles only, the CI shape) attests not_affected from the package-lock.json pin.

Only after a deno install does standalone vex see the installed copy and omit the patch (not_applied).

deno.lock resolves the same name@version from the registry, so this is the "contested lock" case that CLI_CONTRACT.md already handles for npm / pnpm / yarn / bun / vlt. deno.lock just isn't one of the locks it reads.

Impact

  • False VEX. A scanner fed the document suppresses the CVE, while the code Deno actually runs is unpatched.
  • False success. The hosted run's summary ("Switched 1 package to hosted patches") and its exit code tell a Deno user the dependency is patched. docs/ecosystems.md lists Deno hosted mode as "❌ not supported". Nothing in the output says the pin only takes effect for npm ci.
  • Dual Node/Deno repos (a committed package-lock.json plus deno.lock) are common for libraries and apps migrating to Deno 2.

Repro (Linux; Deno 2.9.6 / 2.2.15; npm 10; no API key)

A local stub of the public patch proxy grants one hosted patch for is-odd@3.0.1. The stub serves a patched tarball whose index.js prepends globalThis.__SP=["is-odd-hosted"], plus a patch view with real before/after hashes. It's about 30 lines of http.server, with these routes: POST /patch/batch, GET /patch/by-package/<purl>, GET /patch/view/<uuid>, POST /patch/package returning {status:"granted", url:"$S/patch/npm/<uuid>/is-odd-3.0.1.tgz", artifacts:[{kind:"tarball", integrity:{sha512,…}}]}, and the tarball itself.

SP=/path/to/socket-patch            # main 2463257
export SOCKET_PROXY_URL=http://127.0.0.1:8770 SOCKET_PATCH_SERVER_URL=http://127.0.0.1:8770
mkdir mixed && cd mixed && export DENO_DIR=$PWD/../dd
echo '{"name":"m","version":"1.0.0","dependencies":{"is-odd":"3.0.1"}}' > package.json
echo '{"nodeModulesDir":"manual"}' > deno.json
echo 'import isOdd from "is-odd"; isOdd(3); console.log("loaded-patched="+JSON.stringify((globalThis as any).__SP||[]));' > probe.ts
npm install --package-lock-only --ignore-scripts      # package-lock.json
deno install                                          # deno.lock (v5) + node_modules
git init -q && git add -A && git commit -qm init

$SP scan --mode hosted --json --vex in.vex.json
#   status success, redirect.redirected 1, rewrittenFiles [.npmrc, package-lock.json],
#   warnings [redirect_npm_allow_remote] only. deno.lock is untouched.
#   in.vex.json: not_affected for pkg:npm/is-odd@3.0.1

rm -rf node_modules                                   # = fresh clone
$SP vex --json -O v.json --product pkg:generic/t     # verified / not_affected

deno install --frozen                                 # succeeds; deno.lock unchanged
deno run -A probe.ts                                  # loaded-patched=[]   <- unpatched code runs
$SP vex --json -O v.json --product pkg:generic/t     # now: skipped not_applied (correct)

rm -rf node_modules && npm ci --ignore-scripts && node -e 'require("is-odd")(3);console.log(globalThis.__SP)'
#                                                     # [ 'is-odd-hosted' ]  <- only npm consumes the pin

Expected vs actual

  • Expected:
    • CLI_CONTRACT.md, Contested locks: "When one lock wires a package to a patch and another lock resolves the same name@version from a non-Socket source, the build's bytes depend on which package manager runs. The reference is then dropped with a patched_ref_unattributable diagnostic naming both files." deno.lock's npm section ("is-odd@3.0.1": {"integrity": "sha512-…"}) is exactly such a registry resolution, so lockfile-only and in-run VEX should drop the ref with patched_ref_unattributable.
    • The hosted run should warn that deno.lock will keep installing the registry copy, the way vlt projects get redirect_vlt_sibling_lockfiles and lock-less ones get redirect_npm_no_lockfile. Per docs/ecosystems.md, Deno has no hosted mode.
  • Actual: the redirect is counted, no warning is given, and VEX attests not_affected while Deno runs the unpatched bytes.

Matrix (Linux sandbox, real Deno + real npm, runtime-checked)

Deno hosted scan result in-run --vex vex fresh clone deno install --frozen → code loaded vex after deno install
2.2.15 success, redirected 1, no deno warning not_affected (wrong) not_affected (wrong) unpatched not_applied (ok)
2.9.6 (reproduced 3×) same not_affected (wrong) not_affected (wrong) unpatched not_applied (ok)

Deno 1.46.3 wasn't tested. macOS and Windows weren't probed; nothing here is OS-specific (lockfile discovery only). Releases 3.3.0 and 4.0.0 weren't bisected, since manifest-less hosted VEX is new in v5 (#251).

Suspect code

  • crates/socket-patch-core/src/vex/discover/mod.rs:574 (contest_across_locks): elsewhere is fed by the npm / pnpm / yarn / bun / vlt / Python extractors, and there is no deno.lock reader, so a Deno registry resolution never contests a package-lock.json hosted ref.
  • crates/socket-patch-core/src/vex/discover/npm.rs:87 (push_uncontested): the same pairwise rule within the npm family.
  • The npm hosted rewriter in crates/socket-patch-core/src/patch/redirect/mod.rs has no deno.lock sibling check or warning.

Related: #405 (the same class of problem, where vex attests while the installed copy the runtime uses is unpatched, for Bun's isolated linker).

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions