Node.js Permission Model

Run Node 24 with narrow filesystem grants, inspect permission decisions, distinguish file and directory access, and understand the model limits.

Lesson 41 of 43Intermediate10 min read

A trusted maintenance script may need to read one input file while having no legitimate reason to overwrite unrelated files or spawn another process. Node's permission model can help prevent that trusted code from accidentally using selected resources outside its grants. It is not a security sandbox for malicious JavaScript. This reference pins its behavior and examples to Node 24.11.1, because newer Node documentation includes capabilities not present in that release. The complete fixtures create harmless temporary files and launch a child Node process with narrowly selected flags. The parent performs setup; the restricted child demonstrates actual allowed and denied operations.

Enable restrictions at startup

The --permission flag enables the model. Relevant --allow flags grant access to selected resource categories, such as filesystem reads and writes, child processes, and workers in this release. Permission decisions happen within the Node process running under those flags. An unrestricted parent launching a restricted child is a useful test arrangement, but it does not make the parent restricted. Keep that boundary visible when building deployment commands or test harnesses.

The entry file is included in the permitted startup read behavior documented for this version. Imported application files and ordinary data access still need suitable grants. A real package may require read access to its modules, so inventory its actual startup and runtime paths rather than granting every file immediately when the first error occurs. Run a small controlled operation first, add only justified access, and document the command that reproduces the intended policy. The model's categories and limitations evolve, so check the documentation for the exact Node release being deployed.

Grant paths narrowly and understand directory scope

--allow-fs-read can name an individual file or a directory. A directory grant intentionally permits a broader set of descendants than a single-file grant. An existing directory receives documented descendant behavior during initialization. Wildcards also have specific prefix semantics; do not assume they behave like an arbitrary shell glob with all the filtering features you might expect. A grant resembling a filename pattern can be broader than its author intended.

Separate read and write needs. Reading a configuration file does not require permission to overwrite it. If an output directory is needed, grant that directory's write access deliberately instead of granting writes to the whole filesystem. Resolve paths consistently relative to the intended working directory or supply absolute paths. Startup grants are not a replacement for validating user-selected filenames inside application code. The practice compares a single-file grant with a directory grant using two controlled sibling files, showing why those choices represent different authority even when the immediate task reads only one file.

Inspect decisions and handle denied access

When enabled, process.permission.has can inspect a scope and an optional resource reference. It is useful for explaining a capability to a user or choosing a supported optional path. It does not perform the operation and does not remove ordinary filesystem races or errors. An allowed read can still fail because the file is missing or inaccessible under operating-system permissions. Continue handling actual operation errors rather than treating a capability check as proof that a read must succeed.

A denied operation throws ERR_ACCESS_DENIED. Branch on the documented code where the application has an appropriate recovery path, such as reporting that an optional export is unavailable. Do not automatically catch the error and relaunch with unrestricted permissions. That defeats the policy and can conceal an accidental resource dependency. The main fixture reads its explicitly granted file, then attempts a sibling read and a write. Those failures are expected test outcomes. It prints stable error codes and booleans instead of platform-specific absolute paths or complete stack traces.

Keep the documented limitations in the threat model

Node 24.11.1's model is designed for trusted code and does not restrict every possible way to access every resource. Its documentation describes important limitations involving symbolic links, existing file descriptors, and some initialization behavior. It is not a network allowlist in this pinned release, and this guide does not borrow newer --allow-net or audit-mode behavior from a later major version. Do not run hostile packages under these flags and claim they cannot escape or affect the host.

Operating-system users, containers, external sandboxing, dependency review, and application authorization have different roles. Use the model as one deliberate restriction for trusted programs, with the other controls appropriate to the actual deployment. Granting child-process or worker access expands the effective authority and needs careful review; a spawned process has its own startup policy rather than magically inheriting every intended restriction. Also keep filesystem layout controlled so granted paths do not contain unexpected links. A narrow flag list is useful only when its scope and limitations match the assumption under which the code is trusted.

Node.js documentation and version notes

Node.js Permission Model Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Running a child process with one read grant

Run the complete .mjs controller with Node 24.11.1. It writes a child script and two text files inside a fresh temporary directory, then starts the child with read access to only input.txt in addition to its entry-point behavior. The child successfully reads that input. Reading other.txt and writing result.txt both fail under the requested policy. It also reports that child-process permission has not been granted. The unrestricted controller removes its own generated directory afterward. No real project file is changed, and the example never attempts to bypass the permission model or execute untrusted code.

JavaScript
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
const dir = mkdtempSync(join(tmpdir(), 'permission-demo-'));
try {
  const input = join(dir, 'input.txt');
  const other = join(dir, 'other.txt');
  const result = join(dir, 'result.txt');
  const script = join(dir, 'child.mjs');
  writeFileSync(input, 'approved data');
  writeFileSync(other, 'not granted');
  writeFileSync(script, `import { readFileSync, writeFileSync } from 'node:fs';
const [input, other, result] = process.argv.slice(2);
console.log(readFileSync(input, 'utf8'));
try { readFileSync(other); } catch (error) { console.log(error.code); }
try { writeFileSync(result, 'x'); } catch (error) { console.log(error.code); }
console.log(process.permission.has('child'));`);
  process.stdout.write(execFileSync(process.execPath,
    ['--permission', '--allow-fs-read=' + input, script, input, other, result], { encoding: 'utf8' }));
} finally { rmSync(dir, { recursive: true, force: true }); }
Output
approved data
ERR_ACCESS_DENIED
ERR_ACCESS_DENIED
false

Node.js Permission Model in Real Projects

Compare file and directory read grants

Compare narrow and broad read grants with the same child capability query. The controller creates two sibling data files and runs its child twice: first granting only a.txt, then granting the containing directory. The child prints permission.has for both files. This is a scope exercise rather than a request to read private data. Both files are harmless fixtures created by the controller. The different booleans explain why adding a directory to solve one missing-file error can authorize more than the immediate need. Keep both runs under the same pinned runtime so the comparison reflects policy shape rather than version differences.

JavaScript
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
const dir = mkdtempSync(join(tmpdir(), 'permission-scope-demo-'));
try {
  const first = join(dir, 'a.txt');
  const second = join(dir, 'b.txt');
  const script = join(dir, 'check.mjs');
  writeFileSync(first, 'a'); writeFileSync(second, 'b');
  writeFileSync(script, `const [a, b] = process.argv.slice(2);
console.log(process.permission.has('fs.read', a), process.permission.has('fs.read', b));`);
  for (const grant of [first, dir]) {
    process.stdout.write(execFileSync(process.execPath,
      ['--permission', '--allow-fs-read=' + grant, script, first, second], { encoding: 'utf8' }));
  }
} finally { rmSync(dir, { recursive: true, force: true }); }
Output
true false
true true

Node.js Permission Model FAQ

Can these flags safely contain malicious JavaScript?

No. The documented model assumes trusted code and aims to prevent unintended use of selected resources. It has explicit limitations and is not an adversarial sandbox. Use an appropriate external isolation boundary for hostile code. Do not install an untrusted package, add --permission, and claim the host is protected. The trust assumption is part of the feature's contract, not a detail to omit from an example.

Does read access imply write access?

No. Filesystem reads and writes use separate grants. A script can read its input without being permitted to overwrite it or create an output beside it. Decide which operation and path the task needs. An allowed permission check also does not override operating-system permissions or ensure the file exists. Actual read and write calls still require normal error handling.

Why pin this lesson to Node 24.11.1?

Permission capabilities and limitations change across releases. Newer documentation may describe network controls, audit behavior, or runtime APIs absent from the fixture's release. The examples use only the flags and permission.has behavior verified for this version. For a different deployment version, read that version's documentation and rerun its capability fixtures before claiming the same policy coverage.

Is a directory grant equivalent to one file grant?

No. The practice demonstrates that granting the directory permits both controlled sibling files, while granting one file permits only that file in the queried scope. A directory can also contain more files over time. Choose a directory only when that broader authority is justified and its layout is controlled. Keep unexpected symbolic links and broad wildcard behavior in mind under the documented limitations.

Node.js Permission Model Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What does the worked directory grant report for a.txt and b.txt?

Node.js Permission Model Quiz

2 questions to check what you learned.

  1. What does ERR_ACCESS_DENIED represent in the main child?

    • The selected resource operation was not granted by the enabled model
    • The file must be missing from the operating system
    • The trusted code has been proven malicious
  2. Why avoid responding to every denial with --allow-fs-read=*?

    • It replaces the narrow intended authority with broad filesystem read access
    • The wildcard limits access only to one extension
    • It gives write access but leaves read access unchanged