Why Deno Is "Secure by Default"
In Node.js, any script you run has immediate access to the file system, network, environment variables, and system calls. This means a malicious or buggy dependency can read your .env files, open network connections, or execute shell commands without you knowing.
Deno takes a fundamentally different approach: all access is denied unless explicitly granted. This is called the permission model, and it's one of Deno's defining features.
The Core Permissions
When running a Deno script, you must pass flags to grant access to system resources. Here's a breakdown of all available permissions:
| Flag | What It Allows |
|---|---|
--allow-read | Read access to the file system |
--allow-write | Write access to the file system |
--allow-net | Network access (outbound/inbound) |
--allow-env | Access to environment variables |
--allow-run | Permission to spawn subprocesses |
--allow-ffi | Access to native (FFI) plugins |
--allow-hrtime | High-resolution time measurement |
--allow-sys | Access to system info (OS, hostname, etc.) |
--allow-all / -A | Grants all permissions (use with caution) |
Scoping Permissions
Permissions can be scoped to limit the blast radius if something goes wrong. Rather than granting broad access, be specific:
File System Scoping
# Allow reading only from the /data directory
deno run --allow-read=/data app.ts
# Allow writing only to a specific output folder
deno run --allow-write=./output app.ts
Network Scoping
# Allow outbound connections only to api.example.com
deno run --allow-net=api.example.com app.ts
Environment Variable Scoping
# Allow reading only specific env vars
deno run --allow-env=PORT,DATABASE_URL app.ts
Checking Permissions Programmatically
Deno exposes a permissions API so your code can check and request permissions at runtime:
const status = await Deno.permissions.query({ name: "read", path: "/etc" });
console.log(status.state); // "granted", "denied", or "prompt"
// Request a permission dynamically
const result = await Deno.permissions.request({ name: "write", path: "./logs" });
if (result.state !== "granted") {
console.error("Write permission denied. Exiting.");
Deno.exit(1);
}
The --deny-* Flags
Deno also supports deny flags, which let you block specific resources even when a broader permission is granted. This is useful for auditing and defense-in-depth:
# Allow all network except a specific host
deno run --allow-net --deny-net=internal.corp app.ts
Security Best Practices
- Never use
--allow-allin production. It defeats the entire security model. Grant only what's needed. - Scope file and network permissions narrowly. Use paths and hostnames rather than blanket access.
- Audit third-party modules. Even with permissions locked down, a module can misuse the access it's given. Review what you import.
- Prefer URL pinning. Import from a specific version URL (e.g.,
std@0.220.0) to prevent unexpected updates. - Use
--no-promptin CI/CD. This prevents interactive permission prompts and forces explicit flags in automated environments. - Lock your dependencies. Use a
deno.lockfile to ensure dependency integrity across environments.
The Lock File
Deno can generate a lock file that records the cryptographic hashes of all remote dependencies:
deno cache --lock=deno.lock deps.ts
When running with --lock=deno.lock, Deno verifies every remote module against the recorded hash — preventing supply chain attacks where a dependency is silently modified.
Conclusion
Deno's permission model is one of the most thoughtful security designs in the JavaScript runtime space. By denying access by default and requiring explicit opt-in, it dramatically reduces the attack surface of your applications. Take advantage of scoped permissions, use lock files, and you'll build apps that are safer by design — not just by convention.