We ran Node’s permission model for a week — here’s what broke
Running a real service under file system restrictions surfaced four failures we did not predict — including a dependency spawning a subprocess at import.
Node's permission model asks a question the platform avoided for fifteen years: should a package you installed be able to read your SSH keys? The historical answer was yes, silently, because a Node process has always had whatever access the user running it has.
The flag-based model narrows that. Start a process with restrictions and file system reads, writes, and child process spawning must be granted explicitly. We pointed it at an ordinary internal service — Express, Postgres, a job queue, roughly forty direct dependencies — and ran it that way for a week to find out what a real codebase trips over.
Four things broke. Only one of them was the code we had written.
1. Reads we did not know we were doing
The first failure came from configuration. A dotenv-style loader walked upward from the working directory looking for files, and the walk itself required read access to every directory it touched, including ones that held no configuration at all. The service could not start. The honest reading is not that the loader was badly written. It is that we had never once considered which paths our process touched before doing useful work, because nothing had ever asked us to. Writing the grant list was an exercise in discovering our own program.2. Temporary files with no home
A PDF export wrote to a temporary directory, and a thumbnailer shelled out to a binary that did the same. Both needed write access to a system temp path that appeared nowhere in our source, our configuration, or our documentation. We found them the way everyone finds them: by reading the error, granting the path, and hitting the next one. The fix was better than the grant. Both jobs had reasons to write to a directory we controlled, and moving them there made deployment simpler and cleanup possible for the first time.3. A dependency that spawned a process
This one was worth the entire experiment. A transitive dependency — four levels down, pulled in by something innocuous — attempted to spawn a child process at import time to detect a system capability. Nothing malicious. Also nothing we had agreed to.We had audited that dependency tree twice for licences and known vulnerabilities. Neither audit would have told us that importing a formatting utility starts a subprocess on your machine.That is the actual argument for the permission model, and it is not really about attacks. It is about the difference between what you believe your program does and what it does.
4. The test suite, comprehensively
Tests read fixtures, write snapshots, spin up temp databases, and shell out to helpers. Under restrictions they failed in a cascade, and the grant list required to make them pass was so broad that it granted approximately everything. We stopped trying. Running tests under the same restrictions as production sounds rigorous and mostly measures your test harness. Restrict the deployed artifact; let the test runner do what test runners do.What the week actually produced
The permission list ended up short:node --permission \
--allow-fs-read=/app \
--allow-fs-read=/app/config \
--allow-fs-write=/app/var \
server.js
Four lines that make three claims: this program reads its own directory, writes to one place, and never spawns anything. Those claims are now enforced rather than assumed, and any change that violates one fails loudly at start rather than quietly in production.
The costs were real. Error messages point at the operation that was denied rather than the reason it was attempted, so tracing a failure back to the dependency that caused it took stack reading. Anything relying on dynamic paths needs care. And the model does not protect the network, which is where the data you actually care about would leave.