Deno and Bun both run npm packages, but they make opposite bets on security: Deno denies file, network, environment and subprocess access unless you grant it, while Bun runs with the full authority of its user and focuses its protection on what gets installed. That difference changes how a compromised dependency behaves, what a leaked token is worth and which controls a platform team has to add. This guide compares the two runtimes' security models, permission flags, install-script handling, release-age gates, lockfile controls and credential handling, and gives a hardening baseline for each. It is for developers, platform engineers and AppSec teams adopting either runtime for services, CLIs or CI. Facts come from the Deno and Bun documentation as checked on 27 October 2026, when the latest releases were Deno 2.9.8 and Bun 1.4.4. For the Node.js equivalent see the Node.js Permission Model Guide.
Key takeaways
- Deno is deny-by-default; Bun is not. Deno code has no I/O without
--allow-*flags; Bun's documentation describes no runtime permission flags, so every dependency has the process user's full access. - Some Deno grants equal "allow all". Deno's own docs say
--allow-runand--allow-ffibypass the sandbox, and--allow-run=denolets code start a fresh, unrestricted Deno process. - Both refuse most install scripts by default. Deno runs npm lifecycle scripts only with
--allow-scripts; Bun runs them only for packages intrustedDependenciesor its built-in allow list. - Both can delay brand-new releases. Deno 2.9 enforces a 24-hour minimum dependency age by default; Bun's
minimumReleaseAgeis opt-in and set in seconds. - Neither is a sandbox for hostile code. Deno lets any code run at the same privilege through
evaland dynamic import, and recommends OS sandboxing or a VM for untrusted code; Bun relies on those layers entirely.
How identity and access work in Deno and Bun
A JavaScript runtime acts with the identity of the OS user that starts it, so every package in its module graph can use that user's files, environment variables and network position. The supply chain question is how much of that authority the runtime hands to dependency code.
| Area | Deno (2.9) | Bun (1.4) |
|---|---|---|
| Default runtime access | No file, network, env, subprocess, FFI or system info access | Full access of the OS user |
| Granting access | --allow-read, --allow-write, --allow-net, --allow-env, --allow-run, --allow-sys, --allow-ffi, --allow-import, scoped per resource; --deny-* overrides | Not applicable |
| Denied operation | Throws Deno.errors.NotCapable, or prompts on an interactive terminal | Succeeds if the OS allows it |
| npm lifecycle scripts | Not run by default; deno install --allow-scripts=npm:pkg | Run only for trustedDependencies or the default trusted list |
| New-release gate | Minimum dependency age on by default (24 hours) | minimumReleaseAge opt-in |
| Publishing-trust check | trust-policy=no-downgrade in .npmrc | Pluggable security scanner ([install.security]) |
| Vulnerability audit | deno audit | bun audit |
| .env handling | Loaded only when you pass --env-file | Loads .env, .env.<NODE_ENV> and .env.local files automatically |
| On-the-fly package installs | Remote imports limited to default HTTPS hosts unless --allow-import changes them | Auto-install when no node_modules exists (install.auto = "auto" by default) |
Deno's security principles
Deno's documentation sets out principles that define what its sandbox does and does not promise:
- Same thread, same privilege: all modules in a thread share one permission set; a dependency cannot be given less than your own code.
- No limit on code at the same privilege:
eval,new Function, dynamic imports and workers can run arbitrary code with the caller's permissions. - Static imports skip permission checks: modules in the initial static graph (local files, npm, JSR and remote URLs) load without
--allow-reador--allow-net; non-literalimport(someVariable)is checked at runtime. - No escalation without consent: code cannot gain permissions except through a prompt or a launch flag.
Admin roles and privileged access
Neither runtime has accounts or roles. Privilege is set by whoever controls the launch command, the configuration files and the install step.
Deno grants that act like administrator rights
| Grant | Why it is dangerous | Safer alternative |
|---|---|---|
-A / --allow-all | Turns the sandbox off; Deno says it has the same security properties as Node.js | Explicit scoped flags |
--allow-run (unscoped) | Subprocesses run outside the sandbox; spawning with LD_* or DYLD_* variables requires the unscoped flag | --allow-run=git for named trusted programs |
--allow-run=deno or a shell | Code can start a new Deno process with --allow-all and escape entirely | Never grant to sandboxed code |
--allow-ffi | Native libraries make system calls directly, ignoring all flags | Scope to a library path and treat as full trust |
--allow-write plus --allow-run | Code can overwrite an allowed binary, then run it | No write access to directories holding allowed executables |
DENO_PERMISSION_BROKER_PATH | Hands every permission decision to an external broker; all --allow-* and --deny-* flags and prompts are ignored | Only for central policy services that are audited and highly available |
The permission broker is worth understanding for platform teams: Deno sends each check as a versioned JSON request with process ID, request ID, timestamp, permission and value, and expects allow or deny. If the connection fails or messages are malformed, Deno terminates the process rather than fall back to the flags.
Bun's privileged configuration
trustedDependenciesinpackage.json: each entry is a package allowed to run shell commands at install time. Defining the field replaces Bun's default list rather than extending it, and[]trusts nothing.bunfig.toml: controls auto-install, release age, the security scanner and.envloading. Treat it as security configuration with code-owner review.- Who runs
bun: because Bun has no permission flags, the OS user, container and network policy are the only runtime boundary.
Integrations, API tokens and service accounts
The credentials that matter most are registry tokens, cloud keys and CI tokens. The Shai-Hulud npm Worm used a preinstall script that installed the Bun runtime and launched its payload with it, harvesting GitHub, npm and cloud credentials from files, environment variables and metadata services (ATT&CK T1552.001 and T1552.005). Install-script controls in Deno and Bun target exactly that step.
- Registry credentials: Bun reads
~/.npmrc, the project.npmrcandNPM_CONFIG_TOKENorBUN_CONFIG_TOKEN. Keep tokens out of project.npmrcfiles, use read-only tokens for installs and short-lived publish credentials. See the npm Tokens Security Guide. - Environment secrets in Deno: grant only named variables, for example
--allow-env=DATABASE_URL, or use--ignore-envto make other reads returnundefined. Reading/proc/**/environthrough a symlink requires--allow-env. - Environment secrets in Bun: automatic
.envloading means a stray.env.localin a container image becomes live configuration. Disable it in production with--no-env-fileorenv = falseinbunfig.tomland inject secrets from a secrets manager. - Network egress: in Deno, scope
--allow-netto the hosts a service calls (--allow-net=api.example.com:443); in Bun, enforce egress at the network layer. - Dropping access after start-up (Deno): read configuration, then call
Deno.permissions.revoke({ name: "read" })so later code, including dependencies, cannot reread secret files.
See the Secret Sprawl Measurement Guide for finding credentials that have already spread into repositories and images.
Install-time and supply chain controls
| Control | Deno | Bun |
|---|---|---|
| Lifecycle scripts | Ignored with a warning; allow per package with --allow-scripts=npm:duckdb | Allowed for trustedDependencies or the default list (npm sources only; git:, file: and github: sources must be listed explicitly); --ignore-scripts or install.ignoreScripts = true disables all |
| Release age | On by default since 2.9 (24 hours); "minimumDependencyAge": "P3D" in deno.json or --minimum-dependency-age | minimumReleaseAge = 259200 (seconds) in bunfig.toml, with minimumReleaseAgeExcludes; applies only to new resolution |
| Frozen installs | "lock": { "frozen": true } or --frozen; deno ci | --frozen-lockfile or bun ci; not automatic in CI |
| Publishing-trust downgrade | trust-policy=no-downgrade rejects versions published with a weaker method (plain, provenance, trusted publishing, staged) than the locked baseline | Not documented; use a scanner |
| Pre-install scanning | Not built in | [install.security] scanner = "..."; fatal advisories stop installs, warn exits in CI |
| Offline or hermetic runs | "vendor": true, deno run --cached-only | install.auto = "disable" to stop on-the-fly installs |
Bun's auto-install deserves attention: when no node_modules exists, Bun installs imported packages on the fly into a global cache, and a package with no version in any package.json resolves to latest. Bun turns auto-install off when a security scanner is configured, and you can set install.auto = "disable". See the npm Install-Time Security Guide for the shared registry risks.
Logging and detection
| Signal | Runtime | Use |
|---|---|---|
NotCapable errors | Deno | Catch and log the permission and resource; new values after a dependency update are a red flag |
| Permission broker requests | Deno | Central log of every permission decision across processes |
| "Ignored build scripts" warnings | Deno | Inventory packages that wanted to run install scripts |
Scanner fatal/warn results | Bun | Blocked installs in CI with advisory details |
deno audit / bun audit output | Both | Known vulnerable versions from the lockfile |
| Lockfile diffs | Both | New packages, changed integrity hashes, new trustedDependencies or trust fields in deno.lock |
Metrics:
- Deno services launched without
-A, unscoped--allow-runor--allow-ffi(target 100 per cent). - Number of packages allowed to run install scripts per project (falling).
- Projects with a release-age gate of at least 24 hours and a frozen lockfile in CI.
- Bun projects with a configured security scanner and
.envauto-loading disabled in production. - Time from an audit finding to a merged fix.
Hardening baseline
Deno
- Declare permissions in
deno.jsonor the launch command, scoped to paths, hosts and variables; never ship-A. - Run non-interactive services with
--no-promptso a missing grant fails instead of waiting. - Commit
deno.lock, set"lock": { "frozen": true }, and usedeno ciordeno install --frozenin pipelines. - Keep the default minimum dependency age or raise it (for example
P3D), and settrust-policy=no-downgrade. - Allow lifecycle scripts only per package, after review.
- Override default import hosts with an explicit
--allow-importlist if you do not use them; imports are HTTPS only. - Revoke read and env permissions after initialisation where possible.
- For untrusted code, follow Deno's guidance: limited permissions,
--frozenand--cached-only, a worker with a reduced permission set, OS sandboxing, and a VM or microVM such as gVisor or Firecracker.
Bun
- Run as a non-root user in a container with a read-only file system and egress policy; this is the runtime boundary.
- Set
trustedDependenciesexplicitly (or[]) so the default allow list is not trusted implicitly. - Configure
[install.security]with a scanner and setminimumReleaseAge, for example three days. - Use
bun cior--frozen-lockfilein every pipeline. - Set
install.auto = "disable"in production images and CI. - Disable
.envauto-loading in production and pull secrets from a manager. - Run
bun auditin CI.
Rolling it out
- Discover: in Deno, run the service once in a terminal without flags and record each permission prompt; turn the answers into scoped flags in
deno.json. - Pilot: enforce with
--no-promptin staging and watch forNotCapableerrors; in Bun, enable the scanner and release age on one repository first. - Enforce in CI: add policy checks that fail a build on
-A, unscoped--allow-run, newtrustedDependenciesentries or a missing frozen lockfile. - Roll back: widen a single scoped flag or exclude one package from the age gate, never revert to
-Aor an empty age gate wholesale.
Choosing between them
- Running third-party scripts or plugins: Deno's permission flags give a meaningful extra layer; Bun needs OS isolation for the same result.
- Node.js compatibility and speed of migration: Bun runs most Node.js code unchanged, but you inherit Node.js-style full authority; plan container and network controls first.
- Small teams: Deno's defaults (deny I/O, ignore scripts, 24-hour age gate) give more protection with no configuration. Large organisations should standardise either runtime with a shared baseline config and policy checks on launch flags.
Common misconfigurations
deno run -Ain production: usually added to "make it work"; it erases every advantage Deno has over Node.js.- Granting
--allow-runto call one tool: unscoped, it lets code run anything, including a new unrestricted Deno process. - Assuming Deno blocks dependency loading: static imports load without permission checks, so a malicious module in the graph still runs, just with your granted permissions.
- Extending Bun's trusted list by accident: adding one package to
trustedDependenciesdrops the default list, breaking builds; teams then add many packages without review. - Relying on Bun auto-install in scripts: a one-off script in a directory without
node_modulespullslatestfrom the registry at run time. - Shipping
.envfiles in Bun images: automatic loading turns a forgotten file into production configuration, including old credentials.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | Other |
|---|---|---|---|
| Runtime least privilege (Deno flags, Bun OS isolation) | AC-6, CM-7, SC-39 | PR.PS-01 | ATT&CK T1059.007 JavaScript |
| Install-script control, release age, scanning | CM-7(5), SR-3 | PR.PS-06, GV.SC-07 | ATT&CK T1195.001; OWASP ASVS 5.0 V15.1.2 |
| Lockfiles and integrity | SI-7 | PR.PS-02 | OWASP ASVS 5.0 V15.2.5 |
| Credential exposure | IA-5 | PR.AA-01 | ATT&CK T1552.001, T1552.005 |
| Permission and install logging | AU-12 | DE.CM-09 | CIS Controls v8.1 16.4 |
Practitioner checklist
- Inventory which services and CI jobs run on Deno or Bun, with their versions.
- Deno: no
-A, no unscoped--allow-run, no--allow-run=deno, and--allow-ffitreated as full trust. - Deno: frozen lockfile, minimum dependency age of at least 24 hours,
trust-policy=no-downgrade. - Bun: explicit
trustedDependencies, a configured security scanner andminimumReleaseAge. - Bun:
bun ciin pipelines, auto-install disabled and.envauto-loading off in production. - Both: registry tokens out of project files, read-only for installs, short-lived for publishing.
- Both:
deno auditorbun auditin CI with a fix SLA. - Both: non-root containers with egress controls; VMs or microVMs for untrusted code.
Standards and references
- Deno: Security and permissions
- Deno: Permissions reference
- Deno: Supply chain management
- Bun: Lifecycle scripts
- Bun: Security Scanner API
- Bun: bun install (minimum release age, frozen lockfile)
- Bun: Environment variables
- MITRE ATT&CK: Compromise Software Dependencies and Development Tools
Related NHI Mgmt Group resources: Node.js Security Guide · Malicious Package and Dependency Confusion Guide · Shai-Hulud npm Worm Guide · Container Runtime Security Guide