# Risk map — denoland/deno

> Canonical page: https://codevetter.com/unpack/deno/risk-map

# Risk map — denoland/deno

Scanned commit 461f32ec6174d88b7e43654a054d46d67ae2fdab (2026-09-21). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

The blast radius concentrates in three places: the permission system (an 11.7k-line file that every capability check funnels through), the extension ordering list in `runtime/worker.rs` (silently snapshot-coupled across four files), and the many `unsafe { std::env::set_var(...) }` calls in startup that are only sound because they run before any thread is spawned. Security-sensitive paths include FFI `dlopen`, subprocess spawn with LD_PRELOAD-style env filtering, the permission broker's IPC socket, the Deno Deploy keyring token, the runtime-fetched deploy CLI executed with `allow_all`, and the `node` shim that mutates the process `PATH`. Several performance-motivated lazy-loading decisions (lazy node polyfills, hoisted `registerDeclarativeServer` guard) are fragile in the sense that they encode non-obvious invariants in comments rather than tests.
- `deno deploy` executes code fetched from JSR at runtime with `PermissionsContainer::allow_all` — the version is chosen by `resolve_deploy_cli_version` from `@deno/deploy/meta.json`, so a compromised or unexpected JSR publish would run fully privileged on the user's machine. — [cli/tools/deploy.rs#L59-101](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/tools/deploy.rs#L59-L101)
- Subprocess spawn blocks a real privilege-escalation vector: `check_run_permission` refuses to spawn with any `LD_*`/`DYLD_*` env var set unless `--allow-run` is granted for all, because those allow arbitrary code execution via LD_PRELOAD. — [ext/process/lib.rs#L1524-1560](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/ext/process/lib.rs#L1524-L1560)
- `op_ffi_load` gates on `check_ffi_partial_with_path` and then calls `Library::open` on the (possibly denort-remapped) path — once loaded, the dynamic library runs with full process privileges outside the permission model. — [ext/ffi/dlfcn.rs#L145-175](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/ext/ffi/dlfcn.rs#L145-L175)
- The permission prompter defends against terminal spoofing by escaping bidi and zero-width Unicode formatting controls (U+202E RTL Override, U+2066 isolates, U+200B ZWSP, …) that would otherwise let a malicious path forge the prompt label — the Trojan-Source vector. — [runtime/permissions/prompter.rs#L26-62](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/permissions/prompter.rs#L26-L62)
- `DENO_PERMISSION_BROKER_PATH` connects permission decisions to an external process over an IPC pipe; a connection failure calls `std::process::exit(87)` with a TODO noting the exit code was randomly chosen and undocumented. — [runtime/permissions/broker.rs#L14-70](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/permissions/broker.rs#L14-L70) [cli/lib.rs#L829-841](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L829-L841)
- The Deno Deploy token is stored in the OS keyring under service 'Deno Deploy Token' / user 'Deno Deploy' and is read during tunnel setup; on an `Unauthorized` response the code deletes the credential and re-runs interactive auth. — [cli/tools/deploy.rs#L110-112](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/tools/deploy.rs#L110-L112) [cli/lib.rs#L1411-1420](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1411-L1420) [cli/lib.rs#L1520-1540](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1520-L1540)
- Startup performs multiple `unsafe { std::env::set_var(...) }` calls — `DENO_CONNECTED`, `DENO_COVERAGE_DIR`, `DENO_DEPLOY_CLI_SANDBOX`, `PATH`, and every key/value the tunnel server returns in its metadata — each justified only by a comment asserting single-threadedness; adding a thread earlier in startup would make these unsound. — [cli/lib.rs#L1057-1062](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1057-L1062) [cli/lib.rs#L1540-1552](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1540-L1552) [cli/lib.rs#L479-489](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L479-L489) [cli/node_compat_shim.rs#L290-310](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/node_compat_shim.rs#L290-L310)
- The tunnel injects arbitrary server-supplied environment variables into the process: `for (k, v) in metadata.env { std::env::set_var(k, v) }` with no allowlist, so the tunnel endpoint is effectively trusted to configure the local process. — [cli/lib.rs#L1538-1552](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1538-L1552)
- The `node` PATH shim writes a symlink (Unix) or `.exe`-named shim (Windows) into DENO_DIR and prepends that directory to `PATH`, meaning any process spawned by user code — not just Deno — resolves `node` to the Deno binary; correctness depends on the scan finding no real Node first. — [cli/node_compat_shim.rs#L1-30](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/node_compat_shim.rs#L1-L30) [cli/node_compat_shim.rs#L173-310](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/node_compat_shim.rs#L173-L310)
- The extension registration order in `common_extensions` is a cross-file invariant with no automated check: a comment says it must be kept in sync with `runtime/worker.rs`, `runtime/web_worker.rs`, `runtime/snapshot_info.rs`, and `runtime/snapshot.rs`, and a mismatch surfaces as snapshot corruption rather than a compile error. — [runtime/worker.rs#L1206-1212](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/worker.rs#L1206-L1212) [runtime/snapshot.rs#L59-100](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/snapshot.rs#L59-L100)
- `ext/node`'s lazy/eager split is a hand-maintained list with a documented trap: an explicit `esm` entry that no eager module imports 'would land in the module map unevaluated and fail snapshot validation with NonEvaluatedModules'. — [ext/node/lib.rs#L468-480](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/ext/node/lib.rs#L468-L480)
- `runtime/permissions/lib.rs` is a single 11,756-line module owning every descriptor type, the allow/deny/prompt state machine, and `PermissionsContainer` — the highest-blast-radius file in the repo for security regressions. — [runtime/permissions/lib.rs#L1-200](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/permissions/lib.rs#L1-L200) [runtime/permissions/lib.rs#L4132-4160](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/permissions/lib.rs#L4132-L4160)
- Dead/legacy surfaces still occupy the dispatch table: `DenoSubcommand::Vendor` exists solely to print a removal message and exit 1, and `--unstable` is accepted but warns it was removed in Deno 2.0 with a TODO to hard-error in v2.5. — [cli/lib.rs#L524-528](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L524-L528) [cli/lib.rs#L1090-1098](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L1090-L1098)
- The `registerDeclarativeServer` fast path in `99_main.js` encodes a performance optimisation as a behavioural guard — it skips loading ~430 KB of serve/streams modules unless the default export has an own `fetch` — and the comment explicitly notes `dflt == null` must still call through to preserve the existing TypeError. — [runtime/js/99_main.js#L989-1012](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/runtime/js/99_main.js#L989-L1012)
- `deno test --coverage` does `let _ = std::fs::remove_dir_all(coverage_dir)` on a user-supplied path before creating it; the deletion is unconditional (outside the raw-data-only case) and silently ignores errors. — [cli/lib.rs#L456-472](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/lib.rs#L456-L472)
- The `NodeCommand` shell command silently rewrites `node script.js` inside `deno task` to `deno run -A --unstable-bare-node-builtins --unstable-detect-cjs --unstable-sloppy-imports --unstable-unsafe-proto script.js` — granting all permissions and four unstable flags that the task author never wrote. — [cli/task_runner.rs#L378-420](https://github.com/denoland/deno/blob/461f32ec6174d88b7e43654a054d46d67ae2fdab/cli/task_runner.rs#L378-L420)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
