[agent] Found by the scheduled Deno bug-hunt routine (ledger #308).
Summary
deno install -g (every Deno version tested, 1.46.3 to 2.9.6) resolves a global tool's npm dependencies from Deno's shared npm cache, $DENO_DIR/npm/registry.npmjs.org/<name>/<version>/. socket-patch's global mode never looks there:
False "patched" on Deno 2.9. Deno 2.9 also writes a per-tool <install-root>/bin/.<tool>/node_modules/.deno/... tree. For a tool installed from a local or JSR entrypoint, that tree is a decoy: the tool's deno.json has no nodeModulesDir, and Deno still loads the $DENO_DIR copy (DENO_LOG=debug: Resolved package folder of is-odd@3.0.1 to …/dd/npm/registry.npmjs.org/is-odd/3.0.1). The decoy is the one tree --global-prefix can crawl. Pointing --global-prefix at it reports applied, and vex attests verified / not_affected, while the global tool keeps executing the unpatched bytes. (For an npm: entrypoint tool, Deno 2.9 writes "nodeModulesDir": "manual" and does load the per-tool tree; transitive deps there hit #373.)
Impact
- A user or CI job running
socket-patch scan -g to cover their globally installed Deno tools gets a clean success report with zero findings, so vulnerable global tools stay unpatched with no signal.
- On Deno 2.9, the only workaround that looks like it works gives a false VEX
not_affected attestation for code that isn't patched.
Repro (Linux, Deno 2.9.6; same shape on every version)
export DENO_DIR=$PWD/dd DENO_INSTALL_ROOT=$PWD/root
mkdir tool && printf 'import isOdd from "npm:is-odd@3.0.1";\nconsole.log("isOdd(3)=" + isOdd(3));\n' > tool/t.ts
deno install -g -A -n sptool tool/t.ts
find . -path '*is-odd*' -name index.js
# ./dd/npm/registry.npmjs.org/is-odd/3.0.1/index.js <- what Deno runs
# ./root/bin/.sptool/node_modules/.deno/is-odd@3.0.1/node_modules/is-odd/index.js (2.9 only)
# Manifest + blobs for pkg:npm/is-odd@3.0.1; after-bytes = before + "console.log('SP-PATCHED')"
mkdir cwd && cd cwd && git init -q && python3 mkman.py . ../dd/npm/registry.npmjs.org/is-odd/3.0.1/index.js
socket-patch scan -g --json # success, is-odd never queried (verified with a stub of the batch API)
socket-patch apply -g --offline --json # partialFailure: skipped package_not_installed
for P in ../dd ../dd/npm ../dd/npm/registry.npmjs.org; do
socket-patch apply --global-prefix $P --offline --json # partialFailure: package_not_installed (all three)
done
socket-patch apply --global-prefix ../root/bin/.sptool/node_modules --offline --json # success: applied
../root/bin/sptool # isOdd(3)=true -- no SP-PATCHED, the unpatched DENO_DIR copy ran
socket-patch vex --global-prefix ../root/bin/.sptool/node_modules --offline --product pkg:generic/t --output v.json
# statements: GHSA-… not_affected (inline_mitigations_already_exist)
mkman.py is the 15-line helper in the probe workflow linked below (git-sha256 blobs plus .socket/manifest.json). Reproduced twice locally on Linux (1.46.3, 2.2.15 and 2.9.6), and in the 15-cell probe.
Expected vs actual
- Expected: the maintainers' global-mode contract.
scan -g finds every globally installed package that has a patch, and apply -g / rollback -g / vex -g act on the copy that actually runs. CLI_CONTRACT.md documents --global as "Operate on globally-installed packages", and docs/ecosystems.md lists Deno agent mode as "✅ in place". The deno crawler's own module doc says scan --global --ecosystems deno --global-prefix <path> is how users target Deno globals. At minimum, socket-patch should not report applied / VEX not_affected for a copy the runtime doesn't load.
- Actual: Deno globals are invisible to
-g and to every --global-prefix, and scan -g exits 0 / success. On Deno 2.9, patching the per-tool tree gives a false applied and a false VEX attestation.
| OS |
Deno |
Deno loads $DENO_DIR/npm/registry.npmjs.org copy |
apply -g |
--global-prefix $DENO_DIR / npm / registry.npmjs.org |
per-tool node_modules decoy: apply → tool runs patched? / vex |
| ubuntu / macos / windows |
1.46.3 |
yes (only copy on disk) |
not found |
not found ×3 |
n/a (no per-tool dir) |
| ubuntu / macos / windows |
2.0.6 |
yes |
not found |
not found ×3 |
n/a |
| ubuntu / macos / windows |
2.2.15 |
yes |
not found |
not found ×3 |
n/a |
| ubuntu / macos / windows |
2.4.5 |
yes |
not found |
not found ×3 |
n/a |
| ubuntu / macos / windows |
2.9.6 |
yes |
not found |
not found ×3 |
applied, tool UNPATCHED, VEX not_affected |
All 15 cells reproduce. (On Windows scan -g -e npm also scans 0 packages; that's #434.)
First bad version
Not a regression. Releases 4.0.0 and 3.3.0 behave identically (apply -g → package_not_installed), and no version of the npm or Deno crawler has known the $DENO_DIR/npm/<registry>/<name>/<version> layout.
Suspect code
Related, but distinct: #373 (node_modules/.deno transitive deps in projects), #374 (JSR cache path), #436 (get -g --mode hosted|vendored doesn't refuse).
[agent] Found by the scheduled Deno bug-hunt routine (ledger #308).
Summary
deno install -g(every Deno version tested, 1.46.3 to 2.9.6) resolves a global tool's npm dependencies from Deno's shared npm cache,$DENO_DIR/npm/registry.npmjs.org/<name>/<version>/. socket-patch's global mode never looks there:scan -g/SOCKET_GLOBAL=1only crawls the npm, pnpm, yarn and bun global roots (NpmCrawler::get_global_node_modules_paths). It reportssuccessand never lists the Deno-global package.-g -e denoreportsscannedPackages: 0/success.apply -greportspackage_not_installed.--global-prefixvalue can reach the package. The npm crawler expects<prefix>/<name>/package.json, but Deno's layout is<name>/<version>/package.json.--global-prefixset to$DENO_DIR,$DENO_DIR/npmor$DENO_DIR/npm/registry.npmjs.orgall givepackage_not_installed.-gpath probes only$DENO_DIR/npm/jsr.io(see Deno JSR packages are never found in a real Deno project: the crawler only probes $DENO_DIR/npm/jsr.io, and the matching ./vendor/jsr.io layout from "vendor": true is ignored #374), so it finds nothing either.False "patched" on Deno 2.9. Deno 2.9 also writes a per-tool
<install-root>/bin/.<tool>/node_modules/.deno/...tree. For a tool installed from a local or JSR entrypoint, that tree is a decoy: the tool'sdeno.jsonhas nonodeModulesDir, and Deno still loads the$DENO_DIRcopy (DENO_LOG=debug:Resolved package folder of is-odd@3.0.1 to …/dd/npm/registry.npmjs.org/is-odd/3.0.1). The decoy is the one tree--global-prefixcan crawl. Pointing--global-prefixat it reportsapplied, andvexattestsverified/not_affected, while the global tool keeps executing the unpatched bytes. (For annpm:entrypoint tool, Deno 2.9 writes"nodeModulesDir": "manual"and does load the per-tool tree; transitive deps there hit #373.)Impact
socket-patch scan -gto cover their globally installed Deno tools gets a cleansuccessreport with zero findings, so vulnerable global tools stay unpatched with no signal.not_affectedattestation for code that isn't patched.Repro (Linux, Deno 2.9.6; same shape on every version)
mkman.pyis the 15-line helper in the probe workflow linked below (git-sha256 blobs plus.socket/manifest.json). Reproduced twice locally on Linux (1.46.3, 2.2.15 and 2.9.6), and in the 15-cell probe.Expected vs actual
scan -gfinds every globally installed package that has a patch, andapply -g/rollback -g/vex -gact on the copy that actually runs. CLI_CONTRACT.md documents--globalas "Operate on globally-installed packages", and docs/ecosystems.md lists Deno agent mode as "✅ in place". The deno crawler's own module doc saysscan --global --ecosystems deno --global-prefix <path>is how users target Deno globals. At minimum, socket-patch should not reportapplied/ VEXnot_affectedfor a copy the runtime doesn't load.-gand to every--global-prefix, andscan -gexits 0 /success. On Deno 2.9, patching the per-tool tree gives a falseappliedand a false VEX attestation.OS × version (probe run https://github.com/SocketDev/socket-patch/actions/runs/36833777679)
$DENO_DIR/npm/registry.npmjs.orgcopyapply -g--global-prefix$DENO_DIR/npm/registry.npmjs.orgnode_modulesdecoy: apply → tool runs patched? / vexapplied, tool UNPATCHED, VEXnot_affectedAll 15 cells reproduce. (On Windows
scan -g -e npmalso scans 0 packages; that's #434.)First bad version
Not a regression. Releases 4.0.0 and 3.3.0 behave identically (
apply -g→package_not_installed), and no version of the npm or Deno crawler has known the$DENO_DIR/npm/<registry>/<name>/<version>layout.Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1135get_global_node_modules_paths: no Deno root. And:1201node_modules_paths_sync: a--global-prefixis always treated as anode_modules-shaped dir.crates/socket-patch-core/src/crawlers/deno_crawler.rs:75: the global path is$DENO_DIR/npm/jsr.ioonly (Deno JSR packages are never found in a real Deno project: the crawler only probes $DENO_DIR/npm/jsr.io, and the matching ./vendor/jsr.io layout from "vendor": true is ignored #374).deno_dir()at:238already resolves the rightDENO_DIRper OS and could feed anpm/registry.npmjs.org/<name>/<version>walk.Related, but distinct: #373 (
node_modules/.denotransitive deps in projects), #374 (JSR cache path), #436 (get -g --mode hosted|vendoreddoesn't refuse).