Most Node.js-vs-Deno-vs-Bun comparisons are benchmark posts: requests per second, cold-start time, npm install speed. Almost none of them ask the more consequential question — what happens when a postinstall script in a transitive dependency tries to read ~/.ssh/id_rsa or open a socket to an unfamiliar host. The three runtimes answer that question very differently. Node.js ran on pure ambient authority for its entire 2009–2023 history, giving every script full filesystem, network, and process access with no runtime gate at all; its opt-in permission model only reached stable status in Node 23.5.0, released December 19, 2024. Deno, by contrast, shipped with a default-deny, capability-based sandbox from its 1.0 release in May 2020 — nothing runs without an explicit --allow-* flag. Bun, the newest of the three, has shipped no permission or sandbox model at all as of mid-2026, prioritizing near-total Node/npm compatibility over containment. Given that a single npm install can pull in dozens of transitive dependencies from any of the runtimes' package ecosystems, the differences below are not academic — they determine what a compromised dependency can actually do to your machine.
How does Node.js's permission model actually work?
Node's permission model is opt-in and flag-gated, not a default posture. It shipped experimentally behind --experimental-permission in Node 20, and after several releases of hardening, Node 23.5.0 (December 2024) marked it stable and renamed the flag to --permission. Once enabled, the process is locked down until you grant access back with flags like --allow-fs-read=/path/to/dir, --allow-fs-write, --allow-child-process, and --allow-worker; anything not explicitly allowed throws an ERR_ACCESS_DENIED error at runtime. Node's own documentation (nodejs.org/api/permissions.html) has continued tightening edge cases since stabilization, including symlink resolution — a permitted directory no longer implicitly grants access to symlink targets outside it, closing a path-traversal-style bypass. The catch is adoption: because the model is off by default, the overwhelming majority of production Node deployments in 2026 still run with full ambient authority, exactly as they did in 2015. The capability exists; almost nobody turns it on.
Why is Deno's security model considered the strictest of the three?
Deno denies everything by default and requires an explicit grant for every category of system access: --allow-read, --allow-write, --allow-net, --allow-env, --allow-run, and --allow-ffi each unlock one narrow capability, and Deno's own security docs (docs.deno.com/runtime/fundamentals/security) describe this as the runtime's core design principle since its first stable release. Deno also supports --deny-* flags that carve exceptions out of a broader grant — for example --allow-read --deny-read=/etc lets a script read anywhere except one directory — giving finer-grained control than a flat allow/deny list. Deno 2.5, released in September 2025, extended this further by letting teams declare named permission sets directly in deno.json and apply them at run time with a --permission-set flag, plus a DENO_AUDIT_PERMISSIONS environment variable that writes every permission check to a JSONL audit log. The obvious escape hatch is -A / --allow-all, which disables the sandbox entirely and drops a script back to Node-equivalent ambient trust — a reminder that a security model is only as strong as how consistently it's actually invoked.
Does Deno's sandbox actually stop a malicious dependency?
Not on its own, and this is the detail most comparisons skip. Deno's permission system constrains system calls — filesystem, network, subprocess, FFI — not code provenance. A remote or npm-compatible import is still fetched and executed as ordinary JavaScript inside whatever permission grant the host application already holds. If your app legitimately needs --allow-net to call an API, a compromised dependency running in that same process can use that exact same network permission to exfiltrate environment variables or source files it can read. Deno's module-level checks (import maps, lock files, and the --reload cache-busting flag) help with reproducibility and tampering detection, but they don't create per-package permission boundaries the way, say, a browser extension's manifest scopes a single extension's capabilities. In practice this means a strict --allow-net=api.example.com grant is doing real security work, while a blanket --allow-net with no host list gives a malicious package almost the same exfiltration path it would have in Node.
Where does Bun stand on permissions and sandboxing?
Nowhere, as of mid-2026. Bun ships zero built-in permission or sandbox model — every script gets the same full filesystem, network, and process access Node granted by default before v20. A GitHub issue on oven-sh/bun proposing Deno-style --secure / --allow-net flags has been open for discussion but is unshipped, and the Bun team's public priorities have consistently emphasized Node/npm API compatibility (widely cited in the 95–99% range, though no single authoritative benchmark pins an exact figure) and raw execution speed over runtime containment. Bun does address one adjacent concern: reproducible installs via a lockfile, historically the binary bun.lockb and, since Bun 1.2, an optional human-readable text bun.lock format that's easier to diff and audit in code review. That's a real supply-chain hygiene improvement — you can see exactly what version changed in a PR — but a readable lockfile says nothing about what a package's install script or runtime code is permitted to touch. On the axis this post is about, Bun's posture is functionally identical to pre-2024 Node.
Which runtime should a security-conscious team actually pick?
The honest answer is that runtime choice doesn't remove the need for supply-chain controls — it changes which layer has to carry them. Deno is the only one of the three enforcing default-deny, capability-based sandboxing, and teams running untrusted or third-party code (plugin systems, CI runners executing user-submitted scripts, multi-tenant edge functions) get real, if incomplete, containment from it. Node's stable permission model, generally available since Node 23.5.0, gives teams that flag it on a comparable — though more manually configured — safety net, but its opt-in nature means it protects nothing unless a team deliberately adopts it. Bun currently offers no equivalent, so teams choosing it for performance are implicitly accepting Node's pre-2024 trust model. In all three cases, none of this replaces dependency-level scrutiny: a permission grant that's broad enough for your app to function is often broad enough for a malicious package to do damage too, which is why software composition analysis and SBOM visibility into what's actually in node_modules matter regardless of which runtime executes it.
How Safeguard helps
Runtime permission models are a valuable containment layer, but they don't tell a team which of their dependencies are risky in the first place — that's a supply-chain visibility problem, not a sandboxing one. Safeguard's software composition analysis scans npm, JSR, and other JavaScript package manifests for known CVEs regardless of whether the app runs on Node, Deno, or Bun, and generates CycloneDX-format SBOMs on every build so a team can answer "are we affected" the moment a new advisory drops, instead of re-auditing package-lock.json, deno.lock, or bun.lock by hand. Because the sandboxing differences above only limit the blast radius of a compromised package — they don't stop you from installing one — pairing runtime-level permission grants with continuous SCA and SBOM inventory closes the gap that no single runtime's security model covers on its own.