Acumatica · Nodejs

Node.js 22 Permissions — A Field Guide

What Node's experimental permission model (--permission, --allow-fs-read, --allow-child-process) actually protects against, how it differs from Deno's permissions, and where it still falls short.

John Kihiu12 min read

Node's permission model is one of those features that's been "experimental" for long enough that it's easy to assume it never shipped. It's real, it works for a defined set of cases, and it's worth understanding both what it protects against and where it still lets things through — because the gaps are exactly where a compromised dependency will look.

What --permission actually restricts

Starting the process with --permission flips Node into a restricted mode where filesystem access, child process spawning, worker threads, and a few other capabilities are denied unless explicitly granted with flags like --allow-fs-read, --allow-fs-write, --allow-child-process, and --allow-worker. Each flag takes a path or pattern for the filesystem ones, so you can scope read access to a specific directory rather than granting it globally.

BASH · RUNNING WITH RESTRICTED PERMISSIONS
# Deny everything by default, then grant narrowly
node --permission \
  --allow-fs-read="/app/config" \
  --allow-fs-read="/app/node_modules" \
  --allow-fs-write="/app/logs" \
  server.js

# Attempting an unlisted operation throws at runtime:
# ERR_ACCESS_DENIED: Access to this API has been restricted

The threat model this addresses is a real one: a transitive npm dependency — three levels deep, added by a package you trust — that tries to read your .env file, write to an unexpected path, or shell out to curl and exfiltrate data. Under the permission model, all of those actions throw ERR_ACCESS_DENIED unless you granted them, which turns a silent supply-chain compromise into a loud runtime error.

How this differs from Deno's permission model

Deno's permissions are on by default — a Deno script has zero filesystem, network, or environment access unless you pass --allow-net, --allow-read, and so on at invocation. Node's model inverts that: permissions are opt-in via the --permission flag, and if you don't pass it, the process runs with full, unrestricted access exactly as it always has. That's a meaningful difference in practice — Deno forces every script author to think about the boundary; Node's model only helps you if you remember to turn it on.

The default matters more than the mechanism

The actual permission checks in Node's model are comparable to Deno's in granularity. The gap is adoption: Deno's permissions apply to every script by default, so the ecosystem's dependencies are written assuming a sandbox exists. Node's permission model is opt-in, so most of the npm ecosystem was written without ever being tested against it — expect some packages to break under --permission in ways their maintainers never anticipated.

Where the model still falls short

Network access is the biggest gap — as of Node 22, there's no --allow-net equivalent; the permission model covers filesystem, child processes, and worker threads, but a script with those three locked down can still make arbitrary outbound HTTP requests. There's also no fine-grained network allowlist the way Deno lets you scope --allow-net=api.stripe.com. Some core modules and native addons don't fully respect the permission boundary yet either, which is part of why the feature remains flagged experimental rather than promoted to stable.

Don't treat this as a full sandbox yet

With no network permission gate, a compromised dependency can still exfiltrate data over HTTP even with --permission --allow-fs-read=... locked down tight. Use the permission model as one layer — combined with dependency auditing, lockfile pinning, and outbound network policy at the infrastructure level (firewall rules, egress proxies) — not as the only control.

Wrapping up

Node's permission model is a real, usable boundary for filesystem and process-spawning risk, and it's worth turning on for any service that pulls in third-party dependencies you haven't fully audited. But it's opt-in rather than default, and it doesn't yet gate network access — so don't let adopting `--permission` give you false confidence about a threat model it doesn't fully cover.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.