Trust is earned, not given

A different perspective

2023-02-21 · Projects

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

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.