Secure Development

Node.js Permission Model: Restricting What Your Code Can Do

Node.js finally has an experimental permission model. It is a significant step toward containing supply chain attacks, but it has important limitations.

Alex
Security Researcher
5 min read

For years, the single biggest security criticism of Node.js was that any code running in a Node.js process had unrestricted access to the filesystem, network, environment variables, and child processes. A single compromised npm dependency could read your SSH keys, exfiltrate environment secrets, or download and execute arbitrary binaries.

Node.js 20 introduced an experimental permission model that begins to address this. It is not as comprehensive as Deno's, but it represents a meaningful shift in how Node.js approaches runtime security.

How It Works

The permission model is activated with the --experimental-permission flag. Once enabled, the Node.js process starts with no permissions. Capabilities must be explicitly granted:

--allow-fs-read grants filesystem read access. Can be scoped: --allow-fs-read=/app/data/*.

--allow-fs-write grants filesystem write access. Similarly scopable.

--allow-child-process grants the ability to spawn child processes.

--allow-worker grants the ability to create Worker threads.

When code attempts an operation without the required permission, Node.js throws an ERR_ACCESS_DENIED error rather than performing the operation.

Impact on Supply Chain Attacks

The most common npm supply chain attack pattern is:

  1. Compromise a package (or create a malicious one).
  2. Add an install script that reads sensitive files or connects to an external server.
  3. Exfiltrate credentials, SSH keys, or other sensitive data.

With the permission model enabled, step 2 fails. The install script cannot read files outside the granted paths, cannot connect to external servers (network restrictions are being developed), and cannot spawn processes without --allow-child-process.

This does not prevent all supply chain attacks. Code that abuses legitimately granted permissions -- a web framework that reads your source files and exfiltrates them through an HTTP response, for example -- is harder to prevent with coarse-grained permissions.

Current Limitations

Experimental status. The permission model is flagged as experimental, meaning the API may change between Node.js versions. Production adoption requires accepting this stability risk.

No network permission granularity. As of the latest releases, network access restrictions are still being developed. You cannot yet restrict which hosts a Node.js process can connect to. This is a significant gap because network exfiltration is the most common malicious behavior in supply chain attacks.

Per-process, not per-module. Like Deno, permissions apply to the entire process. You cannot grant one dependency network access while denying it to another. This limits the value when your application legitimately needs broad permissions.

Performance overhead. Permission checks add overhead to every filesystem and process operation. For I/O-intensive applications, this can be noticeable. The overhead is why the model is opt-in rather than default.

Ecosystem compatibility. Many npm packages assume unrestricted filesystem access. Build tools, bundlers, and testing frameworks often need broad read/write permissions. Adopting the permission model may require configuring different permission sets for development, testing, and production.

Practical Adoption

Start by running your application with --experimental-permission and no grants to see what breaks. This reveals the actual capability requirements of your application and its dependencies.

Then build a minimal permission set. For a typical web application:

node --experimental-permission \
  --allow-fs-read=/app/* \
  --allow-fs-write=/app/uploads/*,/tmp/* \
  --allow-child-process \
  server.js

Document why each permission is needed. Review the permission set during dependency updates -- a new dependency that requires broader permissions should trigger a security review.

The npm Install Script Problem

The permission model is most valuable during npm install, where install scripts from untrusted packages execute automatically. Running npm with restricted permissions limits what install scripts can do.

However, npm itself needs filesystem access to install packages, and install scripts need access to their package directories for legitimate build operations (compiling native addons, for example). Finding the right permission balance for npm install is an ongoing challenge.

The most secure approach is to use --ignore-scripts during installation and then selectively run build scripts for packages that need them, in a sandboxed environment.

Comparison With Deno

Deno's permission model is more mature and more granular. It includes network permission scoping (specific hosts and ports), environment variable scoping, and has been a core feature since Deno 1.0 rather than an experimental addition.

However, Node.js's ecosystem is vastly larger. The Node.js permission model, even in its current experimental form, protects against supply chain attacks in the ecosystem where they are most prevalent.

Policy System (Complementary)

Node.js also has an experimental policy system (--experimental-policy) that controls which modules can be loaded and verifies their integrity via Subresource Integrity hashes. This is a different but complementary security mechanism. While the permission model restricts what code can do, the policy system restricts which code can run.

Together, they provide defense in depth: only approved code can load (policy), and that code can only do what it is explicitly allowed to do (permissions).

How Safeguard.sh Helps

Safeguard.sh provides the supply chain visibility layer that complements Node.js's runtime permission model. While the permission model constrains what compromised code can do at runtime, Safeguard.sh identifies compromised or vulnerable dependencies before they reach your runtime. It scans your npm dependencies for known vulnerabilities, generates SBOMs capturing your full dependency tree, and alerts you when a package in your project is affected by a new advisory. Prevention through visibility and containment through permissions work best together.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.