Key takeaways

  • Do use --permission as defense in depth for trusted application and plugin code: our fixture confirmed that it blocked direct out-of-allowlist reads, writes and child creation, and v26.7.0’s audit mode makes rollout rehearsal easier.
  • Do not use it as the sole boundary for model-generated, downloaded or otherwise hostile code; Node’s own documentation explicitly excludes malicious code from its guarantee.
  • Treat audit mode as observability, not containment: it tells you what would be denied, but it does not move the trust boundary or make hostile code safe.
  • Treat network egress, symlinks, inherited descriptors and child-process grants as separate authority paths. Each remained consequential in our pinned test.
  • Put untrusted execution behind an operating-system, container, microVM or remote-isolation boundary with a minimal identity, explicit egress policy, resource limits and disposable state.
  • Approve the complete runner—artifact, kernel boundary, mounts, identity, network, secrets, tools, limits, logs and teardown—not a runtime flag in isolation.
01

The answer: keep the flag, move the trust boundary

Use Node’s permission model to make accidental authority visible and to reduce what trusted code can do by default. Do not describe a Node process started with --permission as a sandbox for AI-generated or adversarial code. The project’s own documentation says the feature does not protect against malicious code and describes it as a seat belt. That sentence should control the architecture review.

For generated code, put the enforceable boundary outside the guest runtime: a dedicated operating-system identity plus a hardened container, sandboxed process, microVM or remote executor appropriate to the consequence. Deny network egress by default, mount only staged inputs, expose outputs through a narrow exchange directory, omit ambient credentials, cap CPU, memory, processes and time, and destroy the environment after the run. --permission can remain inside that boundary as another diagnostic and defense layer.

02

The surprising result was not a bypass—it was the documented contract

The flag feels like a sandbox interface. Starting Node with --permission denies filesystem access, process creation, worker threads, native addons, WASI and the inspector unless the corresponding permission is granted. Filesystem flags accept paths and wildcards. process.permission.has() exposes checks to application code. Since Node 22.13.0 and 23.5.0, the feature is marked Stable.

But API stability is not a security-strength claim. In the first two paragraphs of the pinned permissions page, Node says it trusts code it is asked to run, says malicious code can bypass the model, and frames the control as protection against trusted code unintentionally using undeclared resources. The pinned security policy is equally direct: JavaScript, WASM, native code and dependencies are trusted and inherit the execution user’s privileges. A hostile generated program is outside that threat model before a test begins.

03

Node 26.7.0 turns permissions into a dry run, not a different sandbox

The v26.7.0 release note explicitly calls out documentation for --permission-audit, and the permissions docs now define two operational modes. Enforce mode denies undeclared access with ERR_ACCESS_DENIED. Audit mode performs the same checks but publishes violations through node:diagnostics_channel and continues execution. That is operationally useful because it lets teams inventory what a workload asks for before they flip the deny switch.

I verified the Darwin arm64 tarball against SHASUMS256.txt and reran the local fs-read probe. In my test script, both audit and enforce runs emitted node:permission-model:fs for /etc/passwd; audit mode printed READ_OK, while enforce mode raised ERR_ACCESS_DENIED. The new mode therefore improves rollout ergonomics and observability, but it does not move the trust boundary out of Node. Hostile code is still only contained when the outer runner constrains the whole process tree and its I/O.

04

Chronology: experimental restriction became stable without becoming a hostile-code boundary

Node introduced the permission model in v20.0.0. Pull request 56201, titled “stabilize permission model,” merged on December 12, 2024; the documentation records stable status in v22.13.0 and v23.5.0. The exact runtime in our environment, v24.10.0, was released October 8, 2025. At retrieval, Node’s release index also contained newer v26 releases, so this report deliberately does not generalize our observed behavior to all versions.

The important continuity is the documented scope. Stabilization made a useful capability ready for supported use. It did not remove the seat-belt warning, add a claim of containment against malicious code, or make every operating-system authority a Node permission. Teams should version-pin both the runtime and the security assumption: “Stable” answers whether the API is expected to change abruptly; “hostile code contained” is a different proposition that Node explicitly does not make.

05

What the implementation actually gates

In pinned src/env.cc, permission initialization applies deny rules for native addons, the inspector, child processes, worker threads and WASI unless their allow flags are present. It then applies filesystem read and write allowlists. The CLI and tests cover those paths, including multiple allow flags, wildcard and traversal handling, entrypoint reads, workers, process execution, sqlite extensions, compile cache and symlinks.

That is a meaningful control surface. It can catch a package that unexpectedly writes a cache, a plugin that starts a helper process, or application code that reads a directory omitted from its declared needs. It can also document expected authority in a testable command line. It is not a kernel reference monitor for every way a process can affect the machine. The documentation names several gaps: node:sqlite is not covered by node:fs checks; existing file descriptors bypass the model; initialization occurs after some configuration reads; and relative symlinks under allowed paths can reach arbitrary locations.

06

Our eight-scenario fixture

The repository now includes scripts/audit-node-permission-model.mjs. It creates an allowed directory, a denied directory containing a marker secret, and a relative symlink from the allowed directory to that secret. It also starts a Python HTTP server bound only to 127.0.0.1, opens the secret before launching one guest, and invokes a small probe under --permission. Every expected result is asserted; a mismatch makes the audit fail.

The fixture uses synthetic data and loopback networking only. It is not exploit code and does not contact an external target. The command is simply node scripts/audit-node-permission-model.mjs. On Darwin arm64 with Node v24.10.0, all eight assertions passed. The script prints structured JSON so another environment can preserve its exact version, platform, scenario result, permission error and conclusion.

07

Result table: three useful denials and four authority paths that remained open

The allowed-directory write succeeded. A direct read of denied/secret.txt failed with ERR_ACCESS_DENIED and permission FileSystemRead. A direct write outside the allowlist failed with ERR_ACCESS_DENIED and FileSystemWrite. Calling /bin/cat failed with ChildProcess when --allow-child-process was absent. These are the controls working as designed.

Four results define the boundary. First, fetch reached the loopback HTTP server even though no network permission was granted; network is not among the resources the documented model restricts. Second, reading allowed/relative-link returned the marker from denied/secret.txt. Third, a descriptor opened by the parent and passed as fd 3 returned the same marker. Fourth, adding --allow-child-process let /bin/cat read the denied file and return it to Node even though process.permission.has("fs.read", target) was false. The non-Node child is governed by its operating-system credentials, not the parent’s Node filesystem allowlist.

08

Why the relative-symlink result matters in agent workspaces

Agent runners often grant a broad repository or workspace directory because the task needs recursive reads and writes. Repositories can contain symlinks, package-manager links, generated build trees and mounted paths. Node’s documentation warns specifically that relative symbolic links under a granted path may allow arbitrary filesystem access. Our fixture reproduced that documented behavior with a link created before the restricted process started.

Canonicalizing one requested path in application code is not a complete repair. Links can change, path checks can race filesystem operations, and dependencies may open paths through code the orchestrator does not intercept. Before mounting an untrusted repository, inventory and reject unsafe links, stage ordinary files into a fresh root, make host mounts read-only where possible, and enforce the real boundary through the operating system or virtualized executor. Treat the Node allowlist as an additional check, not the final containment layer.

09

Why network is a first-class permission even when the runtime has no network flag

Generated code that cannot read a host secret may still download payloads, call metadata services, scan reachable internal endpoints, send task content away, consume paid APIs or coordinate through DNS and HTTP. Our loopback test establishes only that fetch was permitted in this configuration; it does not claim internet availability or reachability to any particular service. It is enough to show that --permission alone did not enforce an egress-deny policy.

Put outbound policy below the guest runtime. Start with no route or a deny-all network namespace, then add a proxy or allowlist only when the task contract requires a destination. Block cloud metadata and private address ranges independently, bind DNS to the same policy, cap bytes and requests, record destinations, and do not expose proxy credentials that can be reused elsewhere. Package installation is network execution plus supply-chain authority; prebuild approved images or proxy immutable packages rather than opening general egress mid-run.

10

Child-process permission is an authority cliff

Denying child creation by default is valuable, and our test observed the expected ChildProcess error. But many coding tasks immediately request --allow-child-process so they can run git, a compiler, a test runner or a shell. At that point, a non-Node executable receives the operating-system identity and can use system calls that do not consult Node’s permission object. Our /bin/cat case made the distinction visible with one harmless marker file.

In our shell probe, `/bin/sh -lc "cat /tmp/aag-outside-secret.txt"` printed the marker string even though the parent Node process still denied the same path. Node’s own child-process guard stayed intact for Node-to-Node subprocesses, but the shell regained the operating-system user’s full file authority. The permission model is therefore a policy boundary for Node APIs, not for arbitrary shell commands.

Do not grant a general shell merely because an agent needs one command. Prefer a broker exposing named operations with fixed executable paths, validated arguments, controlled working directories, clean environments, deadlines and output limits. Better still, place the complete process tree inside the external sandbox so a necessary compiler cannot escape the mount, identity, network and resource policy. Child denial inside Node is defense in depth; process-tree containment belongs to the host boundary.

11

Inherited descriptors can smuggle authority into a restricted guest

The pinned documentation says existing file descriptors used through node:fs bypass the permission model. We opened the marker before launching the restricted process, passed it as descriptor 3, and read it successfully even though the path itself was outside the read allowlist. This is expected, documented behavior—not a newly reported Node vulnerability.

Descriptor hygiene is therefore part of runner design. Launch guests with an explicit stdio map, close everything else, and inspect service managers, test harnesses, telemetry agents and language bridges that may pass sockets, pipes or files. A connected socket is network authority even if the guest cannot create a new one. A writable log or cache descriptor can cross an intended mount policy. The least-privilege review must begin before process creation, not at the first JavaScript line.

12

A production runner needs layered gates

A defensible runner begins with provenance: pin the Node binary, dependencies, base image and generated artifact; verify hashes; and keep the guest immutable. Give each run a fresh low-privilege identity and disposable root. Mount only copied inputs, prefer read-only source plus a bounded output directory, remove host sockets, and supply no home-directory or cloud credentials. Enforce egress, syscall or platform policy outside Node. Cap wall time, CPU, memory, files, bytes, processes and output, and terminate the full process tree on expiry.

Above that boundary, retain --permission with the smallest filesystem grants and no child, worker, addon, WASI or inspector permission unless the frozen task proves it needs one. Route necessary operations through narrow brokers. Validate outputs as untrusted, scan diffs and artifacts, and run acceptance tests in a separate environment that does not trust the guest. Log policy decisions and violations without copying secrets. Destroy state after evidence collection and exercise the kill and rollback paths before production.

13

Build, buy, deploy or avoid

Build an internal runner only when the organization can own kernel or virtualization policy, image provenance, patching, quotas, egress, secret isolation, telemetry and incident response. Buy a managed executor when isolation engineering is not differentiating and the provider can supply evidence for tenant boundaries, identities, regions, retention, network controls, limits, patching, forensics, deletion and escape response. In either path, reproduce a bounded adversarial suite against the exact configuration.

Deploy Node --permission as a supplementary control when trusted code benefits from an explicit authority manifest and the runner already contains the whole process. Avoid any design that accepts generated code, passes ambient credentials, grants a writable host workspace or Docker socket, leaves unrestricted egress, and calls the result sandboxed because direct node:fs reads failed. The rollback condition is simple: if the external boundary cannot contain the entire process tree and its I/O, do not execute the code.

14

What this investigation does not prove

Eight synthetic scenarios are not a complete security evaluation. Beyond the fs-read audit/enforce check, we did not test Linux or Windows; containers, sandbox-exec, seccomp or microVMs; node:sqlite; native addons; WASI; worker behavior; inspector paths; race conditions; denial of service; side channels; package managers; environment leakage; or undisclosed bypass techniques. macOS path resolution required the fixture to use the canonical /private/var path rather than the /var symlink; that startup failure was an experiment-design issue, not a product finding.

We also did not execute LLM output. The findings apply to any code outside the runtime’s trust assumption, whether generated by a model, downloaded from a dependency, supplied by a plugin or written by an adversary. A future Node release can change behavior. Re-run the artifact on every supported runtime and operating system, inspect the contemporaneous documentation and threat model, and keep the conclusion bounded to the exact runner configuration.

15

Copy-ready untrusted-code runner authorization record

Complete this AccessAllGPT template for one executor and one code workload. Replace prompts with dated source, configuration, test or operational evidence. A runtime permission flag cannot satisfy an external-isolation gate.

Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].

Code origin; model or supplier; languages; users; data; consequence; whether code is trusted, reviewed, generated or adversarial.

Node version and hash; dependencies; base image or VM; generated artifact; policy bundle; broker version; update and rollback path.

OS identity; container, sandbox, microVM or remote executor; kernel and platform; process-tree containment; host interfaces explicitly absent.

Copied inputs; read-only mounts; output path; symlink policy; home and temp handling; inherited descriptor inventory; teardown evidence.

Default route; DNS; metadata and private-range blocks; destination allowlist or proxy; credentials; byte and request caps; destination logs.

--permission command; filesystem grants; child, worker, addon, WASI and inspector status; reason and test for every exception.

Named commands; executable hashes; validated arguments; environment; working directory; deadlines; output caps; shell prohibition or justification.

CPU, memory, wall time, pids, files, disk, output and spend limits; full-tree termination; denial-of-service tests.

Pinned scenario suite; direct and indirect access attempts; symlinks; descriptors; egress; child processes; secrets; expected denials; exact results and failures.

Untrusted artifact validation; separate acceptance environment; logs and retention; alerts; deletion; patching; incident owner; escape and rollback drills.

Build, buy, deploy with constraints or avoid; approved tasks and data; prohibited authority; residual uncertainty; expiry; re-test triggers.

Primary sources

  1. Node.js v26.7.0 release noteNode.js project · Reviewed: Release headline and changelog bullet documenting --permission-audit audit mode behavior · Retrieved · Supports: The v26.7.0 release note records the documentation change that introduced audit-mode behavior for the permission model.
  2. Permissions — Node.js v26.7.0 documentationNode.js project · Reviewed: Operational modes, runtime API, audit-mode diagnostics-channel mapping and known issues · Retrieved · Supports: The v26.7.0 docs define enforce mode and audit mode, explain that audit mode logs violations without denying access, enumerate the diagnostics-channel names, and retain the symlink and existing-descriptor limitations.
  3. SHASUMS256.txt for Node.js v26.7.0Node.js project · Reviewed: node-v26.7.0-darwin-arm64.tar.gz checksum row · Retrieved · Supports: The downloaded Darwin arm64 binary was verified against the official SHA256 before local execution.
  4. Scripts | npm Docsnpm · Reviewed: How npm handles the scripts field; shell execution behavior on POSIX and Windows · Retrieved · Supports: npm documents that scripts run through /bin/sh on POSIX systems and cmd.exe on Windows, which makes shell policy part of the agent boundary whenever an agent is allowed to invoke package scripts.
  5. Permissions — Node.js v24.10.0 documentationNode.js project · Reviewed: Scope and threat statement; stable status; restricted resources; filesystem allowlists; runtime API; npx behavior; constraints; symlink and inherited-file-descriptor limitations · Retrieved · Supports: The pinned documentation calls the permission model a seat belt for trusted code, explicitly says it does not protect against malicious code, lists the resources it restricts, says node:sqlite can access files outside node:fs checks, and documents relative-symlink and inherited-file-descriptor bypass paths.
  6. Node.js security policy and threat model at v24.10.0Node.js project · Reviewed: Responsible testing; trusted and untrusted elements; treatment of code Node.js is asked to run; inherited execution-user privileges · Retrieved · Supports: The project threat model trusts JavaScript, WASM, native code and dependencies that Node.js is asked to run and says that code inherits the privileges of the execution user. This is incompatible with treating the runtime alone as a hostile-code security boundary.
  7. src,lib: stabilize permission model — pull request 56201Node.js project on GitHub · Reviewed: Title; merge date; release lines; linked documentation history · Retrieved · Supports: The permission model moved to stable through a change merged on December 12, 2024 and documented for Node.js 22.13.0 and 23.5.0. Stable API status describes support maturity; it does not revise the project’s malicious-code exclusion.
  8. Node.js v24.10.0 source tree at commit 9b72b88Node.js project on GitHub · Reviewed: Permission initialization in src/env.cc; permission implementation; CLI documentation; permission tests for filesystem, symlinks, children, workers, addons, WASI, inspector, sqlite and file descriptors · Retrieved · Supports: AccessAllGPT cloned the immutable v24.10.0 source commit. Environment initialization denies addons, inspector, child processes, workers and WASI unless explicitly allowed, then applies filesystem read and write allowlists. The test inventory exercises those controls and known symlink behavior; it does not implement a network permission boundary.
  9. Node.js release indexNode.js project · Reviewed: v24.10.0 release metadata and current release rows at retrieval · Retrieved · Supports: The official index dates v24.10.0 to October 8, 2025. AccessAllGPT used that exact installed runtime; this is a version-pinned result rather than a claim about every supported or future Node.js release.
  10. Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile (NIST AI 600-1)National Institute of Standards and Technology · Reviewed: Publication scope; generative-AI risk-management framing; risk mapping, measurement and management context · Retrieved · Supports: NIST presents the profile as voluntary, cross-sector guidance for incorporating trustworthiness considerations into design, development, use and evaluation. It supplies independent governance context for layered, measured controls; it does not evaluate Node.js or certify a sandbox architecture.

Limitations

This investigation is a source review and bounded synthetic experiment on Node.js v24.10.0 plus a follow-up audit/enforce check on Node.js v26.7.0 for Darwin arm64, not a penetration test, vulnerability disclosure, sandbox certification, cross-version comparison or production assessment. The assertions demonstrate selected documented controls and boundary conditions only. We did not test Linux, Windows, node:sqlite, addons, WASI, workers, inspector, package installation, denial of service, races, side channels, container or VM escape, or every filesystem and network API. The loopback result proves only that this Node permission configuration did not restrict the tested fetch. The symlink, descriptor and malicious-code boundaries are documented by Node; we do not present them as previously unknown vulnerabilities. NIST provides general risk guidance and does not evaluate Node.js. Future runtimes, operating systems and runner configurations can differ, and finite tests cannot establish containment against hostile code.

Disclosures

AccessAllGPT did not receive access, payment, private information or review from the Node.js project, OpenJS Foundation, NIST, a sandbox vendor or an AI coding vendor. The experiment used public source and a locally installed runtime with synthetic files and loopback networking. No model or third-party system was tested or ranked. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI. Publication-wide relationships are listed on the disclosures page.

Further AccessAllGPT guidance

  1. Before You Give a Coding Agent Repository Access
  2. Prompt Injection: Set the Deployment Gates Before Your LLM Can Act
  3. AI Agents vs Workflows: Choose the Smallest Reliable System
  4. Design an Agent Benchmark That Predicts Production
  5. AccessAllGPT Research methodology
  6. Publication disclosures

Continue the research

Get evidence-led updates for teams making production AI decisions.