Node.js, part 19: the permission model — capability-based security for a process
Part 19from the Node.js series · 24 parts in all
Historically a Node process could do anything the user running it could: read any file, open any socket, spawn any binary. That is a lot of power for a build script or a dependency's post-install hook. Node 20 added a permission model — off by default, explicit when on. Part 19 is what it enforces and where it is still not a sandbox.
Opting in, then granting narrowly
# Deny everything not explicitly allowed.
node --permission --allow-fs-read=/app/data --allow-fs-write=/app/tmp server.js
# Nothing at all, beyond the module graph it needs to load:
node --permission tool.js
# Denied operations raise ERR_ACCESS_DENIED, catchable like any error.
The allow-list covers file system reads and writes (whole trees, or
--allow-fs-read=/app/data/*.json globs), child process spawning, worker threads,
and native addons. An attempt to step outside raises ERR_ACCESS_DENIED at the call
site, which means a compromised dependency fails loudly instead of quietly packaging your
~/.ssh directory.
Why this is more useful than it sounds
The threat that matters most in the Node ecosystem is not a remote attacker — it is code
you installed. A build step, a formatter's post-install script, or a CLI you ran once has the
same access as your shell. Running those under --permission with no flags at all
means "load your own code and nothing else", which is exactly the contract a formatter should
have.
What it is not
- Not a sandbox. It restricts the Node APIs. A native addon can
still call
open(2)directly, and a dependency that spawns a shell (where allowed) inherits the power it needs to work around the rest. For hostile code you want a container or a VM. - Not a network policy. There is no network permission yet, so a compromised dependency can still exfiltrate anything it managed to read.
- Not free. It is stable and cheap, but wrapping an ordinary service means enumerating its paths — a worthwhile exercise that reveals how much you did not know your own service touched.
Treat it as a seatbelt for the parts of your toolchain you did not write, not as a boundary against an adversary. Next: the CLI you can write in one file, using only core.