Fuller University  ·  School of Computing Friday, August 7, 2026
Runtimes & Engines

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.

Would we keep it on?

For this service, yes — it costs nothing at runtime and it turned an assumption into a check. For a build tool or a CLI that legitimately touches arbitrary paths, no; the grant list would be wide enough to be meaningless, and a permission list that says "everything" is worse than none, because it looks like security. The habit is worth keeping either way. Spend an afternoon finding out what your process actually opens. Even if you turn the flag off afterwards, you will know something about your own software that you did not know that morning — and in our case, that included a subprocess nobody had ever agreed to run.

The Weekly Module

One email. Every Friday. Just JavaScript.

The week in the language, the runtimes, and the registry — read by 42,000 developers who’d rather be shipping.