A Node.js process normally has every right its operating system user has: it can read any file, open any socket, spawn any program and see every environment variable, including the secrets in them. The Node.js Permission Model, enabled with --permission, lets you restrict file system, network, child process, worker, native addon, WASI and inspector access at process start and drop rights at runtime. This guide explains what each flag does, which Node.js release line supports it, how to roll the model out with audit mode, and, just as important, what it does not protect against. It is for Node.js developers, platform engineers and AppSec teams hardening services, CLIs and build tooling. It goes deeper than the Node.js Security Guide; facts are taken from the Node.js documentation for the v24.x and v26.x lines as checked on 27 October 2026.
Key takeaways
- It is a seat belt, not a sandbox. The Node.js documentation says the model stops trusted code from unintentionally using resources and gives no security guarantees against malicious code, which can bypass it.
- Stable, but features vary by release line. The core model has been stable since v23.5.0 and v22.13.0; audit mode,
permission.drop()and config file support are in v24.x, and--allow-netarrives in the v25 and v26 lines. - Start in audit mode.
--permission-auditreports would-be denials throughnode:diagnostics_channelwithout breaking the application, so you can build an accurate allowlist first. - Grant narrowly, then drop after start-up. Allow only the directories, flags and features a service needs, and call
process.permission.drop()once configuration and secrets are loaded. - Layer it with OS isolation. Symlinks, already-open file descriptors, early-read flags and cross-process inspector signals sit outside the model; containers, separate OS users and seccomp or AppArmor still do the heavy lifting.
How identity and access work in the Node.js runtime
Without the Permission Model, Node.js inherits the authority of the OS account that starts it. Every dependency in node_modules runs with that same authority, which is why one compromised package can read ~/.npmrc, cloud credential files or environment variables (MITRE ATT&CK T1552.001 Credentials In Files). The Permission Model narrows that authority for the whole process. It does not separate one module from another: all code in the process shares the same grants.
| Scope | Flag to grant | Added (per Node.js docs) | Stability note |
|---|---|---|---|
| Enable model (enforce) | --permission | v20.0.0; stable in v23.5.0 and v22.13.0 | Stable |
| Audit mode | --permission-audit | v25.8.0 and v24.20.0 | Enforce wins if both are set |
| File system read / write | --allow-fs-read, --allow-fs-write | v20.0.0; entrypoints implicitly readable from v24.2.0 and v22.17.0 | Stable |
| Child processes | --allow-child-process | v20.0.0; flags passed to children via NODE_OPTIONS from v24.4.0 and v22.18.0 | Active development |
| Worker threads | --allow-worker | v20.0.0 | |
| Native addons | --allow-addons | v21.6.0 and v20.12.0 | |
| WASI | --allow-wasi | v22.3.0 and v20.16.0 | |
| Network | --allow-net | v25.0.0 (present in v26.x docs, not v24.x) | Active development |
| Inspector | --allow-inspector | v25.0.0 and v24.12.0 | |
| FFI | --allow-ffi | v26.1.0 | Only in builds with FFI support |
| OpenSSL STORE loaders | --allow-openssl-store | v26.7.0 and v24.21.0 | Grants broad authority (see below) |
| Environment variables | --allow-env | Documented on the main branch, not yet in a release | Check before relying on it |
Release lines matter for planning. The Node.js release schedule shows v24 moving to maintenance on 20 October 2026, v26 becoming LTS on 28 October 2026, and v22 in maintenance until 30 April 2027. The older experimental policy feature (--experimental-policy, integrity manifests for modules) was removed in v22.2.0, so the Permission Model is the only built-in runtime restriction mechanism today.
What the Node.js project treats as a vulnerability
The Node.js Security Policy states the model is designed to reduce the blast radius of mistakes in trusted application code, not to act as a security boundary against intentional misuse or a compromised process. It triages Permission Model reports in three lanes, which tells you what you can rely on:
| Lane | Meaning | Practical consequence |
|---|---|---|
| Vulnerability | Something Node.js does not trust crosses a permission check without trusted code already running in the process | Fixed as a security issue |
| Security-interest bug | Trusted code uses stable APIs as intended but an invariant is enforced inconsistently, for example one API checks and an equivalent API does not | Fixed as hardening, not automatically a CVE; keep Node.js patched |
| Excluded | Intentional misuse by code already in the process, operator-selected flags, modified execArgv or env, or expecting a sandbox for malicious same-process code | Not fixed; you must control these yourself |
The same policy lists items that are not vulnerabilities: files Node.js writes at operator-chosen locations (such as trace files from --trace-event-file-pattern), SQL-level file access through node:sqlite (for example ATTACH DATABASE), symlinks that resolve to allowed paths, and workers started with modified execArgv or env. It concludes that applications relying on the model must not grant --allow-worker to code they do not trust.
Enforce and audit modes
- Enforce mode (
--permission): any operation without a grant throwsERR_ACCESS_DENIEDwith apermissionproperty such asFileSystemReadand theresourcepath. - Audit mode (
--permission-audit): checks run, nothing is denied, and each violation is published on a diagnostics channel:node:permission-model:fs,:net,:child,:worker,:inspector,:wasi,:addonand:ffi. Audit can be combined with--allow-*flags to audit only part of the surface.
Admin roles and privileged access
The Permission Model has no users or roles of its own. Privilege is decided by whoever controls the process launch: the command line, NODE_OPTIONS, and the configuration file. Treat those as privileged configuration.
- Command line and service units: the flags in a Dockerfile
CMD, a systemd unit or a Kubernetes container spec define the grants. Changes to them should go through the same review as IAM policy changes. - Configuration file: with
--config-file, permissions live in a top-levelpermissionobject (for example"allow-fs-read": ["./foo"],"allow-child-process": true), and its presence enables--permissionautomatically. Because the file usually lives in the project, someone who can change the repository can widen grants; protect it with code owners and review. - Inspector access: enabling the inspector is restricted under the model (the
SIGUSR1listener is not created). Keep--allow-inspectorout of production; a debugger attached to a process can do anything the process can. - Runtime self-restriction:
process.permission.drop(scope[, reference])is irreversible. It only affects future checks: open file descriptors, sockets, child processes and workers stay usable, so close them yourself. You can drop only exactly what was granted; a wildcard grant can only be dropped as a whole scope.
Integrations, API tokens and service accounts
The point of the model for most teams is protecting the credentials a service holds. Three patterns work well:
- Read secrets, then drop access: read
/etc/myapp/config.jsonor a mounted secret file at start-up, then callprocess.permission.drop('fs.read', '/etc/myapp'). A later bug or injected code path can no longer reread the file throughnode:fs. - Deny child processes in services: most web services never need to spawn programs. Leaving
--allow-child-processoff blocks a common post-exploitation step and accidental shell-outs; when children are allowed, from v24.4.0 and v22.18.0 the parent's permission flags are passed to Node.js children throughNODE_OPTIONS. - Restrict network where your line supports it: on v25 and later, omitting
--allow-netstops a build script or report generator from calling out. In v26.x it is an all-or-nothing switch, so egress filtering at the network layer is still needed for allowlisting destinations.
Environment variables deserve a specific warning. The --allow-env flag and the behaviour of stripping unlisted variables at start-up are documented on the Node.js main branch but carry no release version yet, so on v24.x and v26.x a restricted process can still read process.env. Until that ships in your line, prefer file-based secrets you can drop, or a secrets manager call made once at start-up. See the Secrets Management Guide.
Two integrations widen authority beyond what the flags suggest. --allow-openssl-store lets OpenSSL STORE loaders (for example a PKCS#11 provider used by crypto.createPrivateKey()) access files, devices, tokens or the network, and that access is not constrained by the fs or net scopes. Native addons and FFI run native code that the model cannot inspect, so --allow-addons and --allow-ffi effectively remove the seat belt for anything that code does.
Using the model with npx and package tooling
To run a package executable under the model, pass flags through npx: npx --node-options="--permission --allow-fs-read=$(npm config get cache)" package-name, which sets NODE_OPTIONS for spawned Node.js processes without affecting npx itself. This is a useful way to run one-off CLIs from the registry with read-only access. It does not make install scripts safe: lifecycle scripts are a package manager concern covered in the npm Install-Time Security Guide.
Logging and detection
| Signal | How to capture | What it tells you |
|---|---|---|
| Audit violations | Subscribe to node:permission-model:* channels and forward permission and resource to logs | The exact grants a service needs; unexpected resources in production audit runs |
ERR_ACCESS_DENIED errors | Error handler and APM; alert on new permission/resource pairs | Regressions after upgrades, or code (including dependencies) trying something new |
| Runtime grants check | process.permission.has('child') etc. in a health endpoint | The process really started with the intended grants |
| Launch configuration drift | Policy-as-code checks on Dockerfiles, Helm charts and node.config.json | Someone widened a grant, for example --allow-fs-write=* |
Metrics that show the control is working:
- Percentage of production Node.js services running with
--permissionin enforce mode. - Services with wildcard file system grants (
*) or--allow-child-process(falling). - Services that drop secret file access after start-up.
- Distinct audit violations per service before enforcement (should converge to zero).
- Mean time to triage a new
ERR_ACCESS_DENIEDpattern.
Hardening baseline
Rollout
- Pick the release line: confirm which flags your Node.js version supports using the table above; plan audit mode on v24.20.0 or later.
- Audit in staging: run with
--permission-auditand a subscriber logging every violation through realistic test traffic and batch jobs. - Write the allowlist: turn findings into explicit paths, for example
--allow-fs-read=/app --allow-fs-read=/etc/myapp --allow-fs-write=/tmp/myapp/, avoiding*. - Enforce in a canary: switch one instance to
--permission, watchERR_ACCESS_DENIEDrates, then widen the ring. - Add runtime drops: drop secret paths and the
childscope after initialisation. - Roll back: revert to audit mode by flag change only; do not remove the subscriber, so you keep visibility.
Path rules worth knowing
- Entrypoints: the main script and
-rpreloads are readable without a grant on current lines. - Wildcards: anything after
*is ignored, so/home/*.jsbehaves like/home/*. Existing directories get an implicit trailing wildcard; for a directory that does not exist yet, add/*yourself. - Comma-separated paths: no longer accepted since v20.7.0; repeat the flag instead.
- Descriptor operations:
fsync,fdatasync,fchmod,fchownandfutimesare disabled under the model because they cannot be tied to a path; libraries that rely on them will fail.
Where it sits next to other controls
| Control | Granularity | Stops malicious code? | Use for |
|---|---|---|---|
| Node.js Permission Model | Process, by resource type and path | No (by design) | Least privilege for trusted code, limiting mistakes |
| Container with read-only root, non-root user | Process tree, file system | Partly | Baseline for every service |
| seccomp, AppArmor or SELinux profile | System calls and files | Stronger, kernel enforced | Untrusted or high-risk workloads |
| Network policy or egress proxy | Destinations | Yes for network | Allowlisting outbound hosts |
| Separate process or VM per tenant | Whole workload | Strongest | Running customer-supplied code |
Deno exposes similar permission flags with different defaults; see the Deno and Bun Runtime Security Guide for a comparison.
Common misconfigurations
- Treating it as a sandbox for untrusted code: the documentation is explicit that malicious code can bypass the model. Plugins, user scripts or AI-generated code need OS or VM isolation.
- Granting
*to make errors go away:--allow-fs-read=* --allow-fs-write=*leaves only the child, worker and addon restrictions. - Relative symbolic links in granted paths: symlinks are followed even outside granted paths, and relative symlinks may expose arbitrary files. Ensure no granted directory contains them.
- Workers launched with custom
execArgv: a worker withexecArgvset, even to[], does not keep the parent's grants unless the flags are listed again. - Forgetting pre-initialisation inputs:
--env-file,--openssl-configand V8 flags set throughv8.setFlagsFromStringare processed outside the model; already-open file descriptors also bypass it. - Assuming other file paths are covered: the model guards
node:fs; the documentation notes it does not guarantee access through other means such asnode:sqlite, and run-time loadable SQLite extensions are blocked. - Same OS user for sandboxed and sensitive processes:
process._debugProcess(pid)is not gated, so a restricted process can force another Node.js process of the same user to open its inspector. Run them as different users.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | Other |
|---|---|---|---|
| Least privilege for runtime | AC-6, CM-7 | PR.PS-01 | OWASP ASVS 5.0 V15.2.5 |
| Process isolation (layered) | SC-39 | PR.PS-01 | ATT&CK T1059.007 JavaScript |
| Protecting credentials on disk | AC-6 | PR.AA-05 | ATT&CK T1552.001, T1005 |
| Dependency risk | CM-7(5) | PR.PS-06 | ASVS V15.1.2, V15.1.5; ATT&CK T1195.001 |
| Audit mode and violation logging | AU-12 | DE.CM-09 | ISO/IEC 27001:2022 A.8.15 |
Practitioner checklist
- Record which Node.js line each service runs and which permission flags it supports.
- Run every service through
--permission-auditwith a diagnostics channel subscriber before enforcing. - Enforce
--permissionin production with explicit paths and no*grants. - Leave
--allow-child-process,--allow-addons,--allow-ffiand--allow-inspectoroff unless justified in writing. - Drop secret file paths and unused scopes with
process.permission.drop()after start-up, and close open handles. - Protect
node.config.json, Dockerfiles and service units with code owners. - Check granted directories for relative symlinks.
- Keep containers non-root with read-only file systems and egress controls; run untrusted code in separate OS users or VMs.
- Alert on new
ERR_ACCESS_DENIEDpermission and resource pairs.
Standards and references
- Node.js: Permissions documentation
- Node.js: Command-line options
- Node.js: Security Policy and threat model
- Node.js: Release schedule
- Node.js: v22 changelog (policy removal)
- OWASP: Application Security Verification Standard 5.0
- MITRE ATT&CK: Command and Scripting Interpreter: JavaScript
Related NHI Mgmt Group resources: Node.js Security Guide · npm Tokens Security Guide · Malicious Package and Dependency Confusion Guide · Secrets Management Guide