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:

FlagWhat It Allows
--allow-readRead access to the file system
--allow-writeWrite access to the file system
--allow-netNetwork access (outbound/inbound)
--allow-envAccess to environment variables
--allow-runPermission to spawn subprocesses
--allow-ffiAccess to native (FFI) plugins
--allow-hrtimeHigh-resolution time measurement
--allow-sysAccess to system info (OS, hostname, etc.)
--allow-all / -AGrants 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

  1. Never use --allow-all in production. It defeats the entire security model. Grant only what's needed.
  2. Scope file and network permissions narrowly. Use paths and hostnames rather than blanket access.
  3. Audit third-party modules. Even with permissions locked down, a module can misuse the access it's given. Review what you import.
  4. Prefer URL pinning. Import from a specific version URL (e.g., std@0.220.0) to prevent unexpected updates.
  5. Use --no-prompt in CI/CD. This prevents interactive permission prompts and forces explicit flags in automated environments.
  6. Lock your dependencies. Use a deno.lock file 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.