Developer Security

Deno's Permission-Based Security Model: What It Gets Right and Where It Falls Short

Deno was built with security as a first-class concern, requiring explicit permissions for file, network, and environment access. Here is an honest assessment of what that model delivers in practice.

Bob
Runtime Security Specialist
6 min read

Security by Default, Not by Afterthought

When Ryan Dahl announced Deno in 2018, he listed "security" among the key things he regretted about Node.js. Node gives every script unrestricted access to the file system, network, environment variables, and child processes. A single compromised npm package can exfiltrate your SSH keys, read your environment variables, and phone home to a command-and-control server — all without any permission prompt.

Deno was designed to fix this. By default, a Deno script cannot read files, make network requests, access environment variables, or spawn subprocesses. Each capability must be explicitly granted through command-line flags. This permission-based model represents the most significant security architecture difference between Deno and Node.js.

Five years in, it is worth examining what this model actually delivers.

The Permission Model in Detail

Deno's permissions are granted at startup through CLI flags:

  • --allow-read — File system read access (optionally scoped to specific paths)
  • --allow-write — File system write access (optionally scoped)
  • --allow-net — Network access (optionally scoped to specific hosts/ports)
  • --allow-env — Environment variable access (optionally scoped to specific variables)
  • --allow-run — Subprocess execution (optionally scoped to specific binaries)
  • --allow-ffi — Foreign function interface access
  • --allow-sys — System information access
  • -A or --allow-all — Grant all permissions (defeats the purpose)

The scoping capability is particularly powerful. You can allow network access to api.example.com:443 while blocking all other network communication. You can allow reading from /app/data while preventing access to /etc/passwd or ~/.ssh.

Runtime Permission Prompts

Deno also supports interactive permission prompts. If a script attempts an action without the corresponding permission, Deno can prompt the user to grant or deny access at runtime. This provides a fallback for scenarios where the required permissions are not known at startup.

What the Model Gets Right

Supply Chain Attack Mitigation

The most significant security benefit of Deno's permission model is supply chain attack mitigation. In Node.js, if a dependency is compromised and contains code that reads environment variables and sends them to an external server, that code executes silently. In Deno, the same attack requires both --allow-env and --allow-net permissions. If the application does not need network access, the attack is blocked.

This is not theoretical. Multiple real-world npm supply chain attacks — including event-stream, ua-parser-js, and colors.js — would have been partially or fully mitigated by Deno's permission model.

Principle of Least Privilege

Deno enforces least privilege at the runtime level. A CLI tool that processes local files needs --allow-read and --allow-write but should not need --allow-net. If you grant only the permissions the application needs, compromise of the application or its dependencies is contained to those capabilities.

Explicit Security Boundaries

The permission model makes security boundaries visible and auditable. You can look at a Deno command line and immediately understand what capabilities the application has. This transparency is valuable for security reviews and compliance.

Where the Model Falls Short

Permission Granularity Gaps

While path-scoped file permissions and host-scoped network permissions are useful, some capabilities lack granularity:

  • --allow-run can be scoped to specific binaries, but once a subprocess is spawned, Deno cannot control what that subprocess does. If you allow running git, the git process has unrestricted access.
  • --allow-ffi is all-or-nothing for native library loading. FFI access bypasses the entire permission model since native code can make arbitrary system calls.
  • --allow-env grants access to read and write environment variables, but cannot distinguish between the two operations.

The --allow-all Escape Hatch

Many developers and many tutorials use -A to grant all permissions because it is easier than figuring out the exact permissions needed. This completely negates the security model. The convenience of --allow-all undermines the discipline the model is designed to enforce.

Documentation, tutorials, and even official examples sometimes default to -A, normalizing its use.

Dependency Code Shares Permissions

Deno's permissions are process-wide, not per-module. If your application needs network access for its own API calls, every dependency in the process also gets network access. A compromised dependency can use the permissions you granted for legitimate purposes.

This is better than Node.js — where everything is allowed — but it is not true sandboxing. True per-module isolation would require a fundamentally different architecture.

Import Map Trust

Deno's URL-based imports and import maps introduce trust decisions that are adjacent to the permission model. When you import from https://deno.land/std/, you trust the CDN, the registry, and the TLS infrastructure. Import maps can redirect those imports to different sources. Compromising an import map effectively compromises the entire application.

Node.js Compatibility Mode

Deno's Node.js compatibility layer (node: specifiers and npm package support) creates tension with the permission model. npm packages were not designed with permissions in mind, and running them in Deno often requires broad permissions that reduce the security benefit.

Comparison with Node.js Experimental Permissions

Node.js has introduced an experimental permission model inspired by Deno. The --experimental-permission flag enables similar file system and child process restrictions. However, as of late 2023, this feature is experimental, lacks network permission scoping, and is not widely adopted.

The existence of Node.js's experimental permissions validates Deno's design but also highlights the difficulty of retrofitting security into an ecosystem that was built without it.

Practical Recommendations

Never use --allow-all in production. Take the time to determine the minimum permissions your application needs. Start with no permissions and add them as needed based on runtime errors.

Scope permissions narrowly. Use path-scoped file permissions and host-scoped network permissions. --allow-read=/app/data is dramatically more secure than --allow-read.

Audit permission requirements of dependencies. If a markdown rendering library requires network access, that is suspicious. The permissions your application needs should align with its functionality.

Use deno.json configuration. Define permissions in your Deno configuration file to ensure consistency across development, CI, and production environments.

Monitor for permission escalation. If your application's required permissions grow over time — especially through dependency updates — investigate why.

How Safeguard.sh Helps

Deno's permission model protects the runtime, but you still need visibility into the software components you consume. Safeguard generates SBOMs for Deno applications, tracking both URL-based imports and npm dependencies used through Deno's compatibility layer. When a vulnerability is discovered in a module you import — whether from deno.land, esm.sh, or npm — Safeguard alerts your team. Combining Deno's runtime permissions with Safeguard's supply chain visibility creates defense in depth: permissions limit what compromised code can do, while Safeguard helps prevent compromised code from entering your application in the first place.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.