Open-Source Wikis

/

Node.js

/

Systems

/

Permission Model

nodejs/node

Permission Model

Owners: @nodejs/security-wg. Public docs at doc/api/permissions.md.

Purpose

Provide a process-wide capability sandbox that restricts what a Node process can do at runtime. When --permission is on, every privileged operation passes through a centralized check that consults a Permission table on the Environment. Without the right grant, the operation throws ERR_ACCESS_DENIED.

Directory layout

src/permission/
  permission.{cc,h}                // central Permission registry
  permission_base.h                // PermissionBase abstract class
  addon_permission.{cc,h}          // dlopen/dlsym for addons
  child_process_permission.{cc,h}  // spawn/exec*
  ffi_permission.{cc,h}            // FFI dlopen
  fs_permission.{cc,h}             // fs read/write paths
  inspector_permission.{cc,h}      // inspector enable
  net_permission.{cc,h}            // outbound/inbound
  wasi_permission.{cc,h}           // WASI calls
  worker_permission.{cc,h}         // new Worker(...)
lib/internal/process/permission.js  // JS bridge (process.permission.has, .deny)

Key abstractions

Type / file Role
Permission (permission.cc) Per-Environment registry of PermissionScope → PermissionBase.
PermissionBase (permission_base.h) Abstract base for each domain; subclasses implement Apply / is_granted.
PermissionScope (permission.h) Enum of domains (FileSystemRead, FileSystemWrite, ChildProcess, ...).
THROW_IF_INSUFFICIENT_PERMISSIONS (macro) The macro every privileged code path uses to gate access.
process.permission (lib/internal/process/permission.js) JS-side has/deny API.

How it gates a call

graph LR
    JSC["JS: fs.readFile('/etc/passwd', cb)"] --> JSGate["process.permission.has?"]
    JSGate --> Bind[fs binding]
    Bind --> Macro[THROW_IF_INSUFFICIENT_PERMISSIONS]
    Macro --> Reg[Environment::permission]
    Reg --> Domain[FsPermission::is_granted]
    Domain -- denied --> Throw[ERR_ACCESS_DENIED]
    Domain -- granted --> Op[uv_fs_open]

The macros in src/permission/permission.h come in synchronous, async-callback, and return-error variants. Every C++ binding that performs a privileged operation must call the appropriate macro before doing the operation; new bindings are reviewed for this.

CLI surface

Flag Purpose
--permission Enable the model.
--allow-fs-read=<path> Add a glob / path to the FS-read allow list.
--allow-fs-write=<path> Add a glob / path to the FS-write allow list.
--allow-net=<host:port> Add a network allow list entry.
--allow-net-server Allow server-side listen.
--allow-net-client Allow outbound connections.
--allow-child-process Permit child_process.spawn/exec.
--allow-worker Permit new Worker(...).
--allow-wasi Permit node:wasi.
--allow-ffi Permit FFI dlopen.
--allow-addons Permit native addon loading.
--allow-inspector Permit attaching the inspector.
--permission-deny=<scope> Deny a previously-granted scope.

By default --permission denies every domain except where the runtime cannot avoid it (snapshot deserialization, internal startup). The full default policy is in src/permission/permission.cc.

FS path matching

The FS domain uses an interval tree (FsPermission::Tree in fs_permission.cc) to match path globs efficiently, including ** and brace expansions. It also normalizes paths to absolute, resolves .. away, and follows POSIX/Win casing rules.

Integration points

  • fs: THROW_IF_INSUFFICIENT_PERMISSIONS(env, kFileSystemRead, path) is sprinkled in src/node_file.cc.
  • child_process and worker_threads: gated at Spawn/Worker::New.
  • inspector: Agent::Start checks the inspector domain.
  • JS-side checks: process.permission.has('net.client'), process.permission.has('fs.read', '/some/path'), etc.
  • Diagnostics channel: node:permission events fire when access is denied (so monitoring can capture sandbox escapes).

Entry points for modification

  • New domain? Add <name>_permission.{cc,h} under src/permission/, register it in permission.cc, expose it through process.permission in lib/internal/process/permission.js, and document at doc/api/permissions.md.
  • New gating point? Wrap the privileged operation in THROW_IF_INSUFFICIENT_PERMISSIONS (or the async variant). Add a regression test under test/parallel/test-permission-*.js.
  • CLI flag? Edit src/node_options.cc plus the corresponding domain's setup.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Permission Model – Node.js wiki | Factory