NodeJSThe Permission Model

The Permission Model

Node.js 20 introduced an experimental Permission Model — a runtime capability system that restricts what a Node.js process can do. By default Node has unrestricted access to the filesystem, network, child processes, and environment variables. With the permission model, you explicitly grant only the permissions the process needs — a form of least-privilege sandboxing. If the process tries to access something not granted, it throws a ERR_ACCESS_DENIED error rather than silently succeeding.

Enabling the permission model

Bash
# Enable with --experimental-permission (Node 20+):
node --experimental-permission server.js
# Without any --allow-* flags, almost all access is denied.

# Grant specific permissions:
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/logs \
  --allow-net \
  server.js
(node:12345) ExperimentalWarning: The Permission Model is an experimental feature and might change at any time
Server listening on :3000
The permission model is experimental — the API and flag names may change between Node versions; do not rely on it for production security without tracking its stability status
As of Node 20/22, the Permission Model is marked **experimental**. It is being actively developed and the flags, error codes, and behavior may change in future releases. It's worth learning and experimenting with today, but treat it as a defence-in-depth layer rather than a primary security boundary in production. For critical security isolation, use container-level restrictions (`seccomp`, `AppArmor`, non-root users), separate processes, or Wasm sandboxing rather than depending solely on an experimental Node feature.
Available permission flags

Flag

What it grants

--allow-fs-read[=path]

Read access to filesystem; omit path to allow all reads

--allow-fs-write[=path]

Write access to filesystem; omit path to allow all writes

--allow-net[=host:port]

Outbound network connections; omit host to allow all

--allow-child-process

Spawn child processes (child_process.spawn, exec, fork)

--allow-worker

Create Worker Threads

--allow-addons

Load native addons (.node files)

--allow-env[=VAR]

Read environment variables; omit name to allow all

--allow-wasi

Use WASI (WebAssembly System Interface)

Filesystem permission examples

Bash
# Allow reading only from /app and /etc/ssl (for TLS certs):
node --experimental-permission \
  --allow-fs-read=/app,/etc/ssl/certs \
  server.js

# Allow writing only to /app/logs and /tmp:
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/logs,/tmp \
  server.js

# Wildcard — allow read from all paths under /app:
node --experimental-permission --allow-fs-read=/app/* server.js

TS
// Programmatic permission check before accessing:
import fs from 'node:fs'

// Check if read access is granted before attempting:
if (process.permission.has('fs.read', '/etc/passwd')) {
  const content = fs.readFileSync('/etc/passwd', 'utf8')
} else {
  console.log('No permission to read /etc/passwd')
}

// Without the check, accessing a denied resource throws:
try {
  fs.readFileSync('/etc/shadow')
} catch (err: any) {
  console.log(err.code)   // ERR_ACCESS_DENIED
}
Network permissions

Bash
# Allow outbound connections only to your API and database:
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-net=api.example.com:443,db.internal:5432 \
  server.js

# Allow all outbound network (but still restrict filesystem):
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/logs \
  --allow-net \
  server.js
Network permission restrictions apply to outbound connections — inbound connections (the server listening on a port) are not restricted by --allow-net
`--allow-net` controls what hosts your code can connect **to** as a client. It does not restrict inbound connections to your server. A server `listen()` call works regardless of `--allow-net`. This is intentional — the permission model focuses on what resources your code can access, not on network topology. If you need to restrict inbound connections, use a firewall or network policy at the OS or container level.
Environment variable restrictions

Bash
# Allow access to specific environment variables only:
node --experimental-permission \
  --allow-env=DATABASE_URL,PORT,NODE_ENV \
  server.js

# Process attempting to read an undeclared env var:
# process.env.SECRET_KEY → throws ERR_ACCESS_DENIED

TS
// Check which permissions are active at runtime:
console.log(process.permission.has('fs.read'))         // true if --allow-fs-read given
console.log(process.permission.has('fs.read', '/tmp')) // true if /tmp is in the allow list
console.log(process.permission.has('net'))             // true if --allow-net given
console.log(process.permission.has('child'))           // true if --allow-child-process given
Practical use cases
  • Running user-provided scripts — restrict them to only read a sandboxed directory and make no network calls.

  • Defence-in-depth for microservices — a billing service should not be able to read the authentication service's key files, even if compromised.

  • Preventing supply-chain attacks — an npm package that tries to exfiltrate secrets via an outbound HTTP call is blocked if --allow-net is scoped to your own APIs.

  • CI/CD scripts — run build scripts that should only touch the project directory, not the rest of the filesystem.

  • Auditing existing code — run with --experimental-permission and no --allow-* flags to discover what your code actually accesses.

Next
Bundle a Node.js application into a single self-contained binary: [Single Executable Applications](/nodejs/single-executable).