Node.js Permission Model: process.permission.drop() in Node 26.3
Security Deep-Dive · Updated 2026

Node.js Permission Model:
process.permission.drop() in Node 26.3

The Node.js Permission Model has graduated from experimental to stable in Node.js 26.3. Learn how to use --permission, process.permission.has(), and the new irreversible process.permission.drop() API to secure your applications.

Oleg Maximov June 5, 2026 14 min read

Introduction: The Seat Belt for Node.js Applications

For years, securing a Node.js server meant relying on OS-level sandboxing (seccomp, AppArmor, containers) or third-party modules. The runtime itself had no built-in mechanism to say "this process should be able to read /var/www but not /etc/passwd".

That changed with the Node.js Permission Model — a runtime security layer that controls access to file system, network, child processes, worker threads, native addons, WASI, FFI, and the V8 inspector. When enabled via the --permission flag, the model follows a default-deny approach: grant individual capabilities explicitly.

With Node.js 26.3.0 (released June 1, 2026), the Permission Model reached Stability 2 — Stable. The --experimental-permission flag was deprecated; --permission is now a first-class, production-ready feature.

How the Permission Model Works

The Permission Model takes a "seat belt" approach: it prevents trusted code from unintentionally accessing resources that haven't been explicitly granted. It does not protect against malicious code — Node.js trusts any code it is asked to run, and the permission model cannot prevent a determined attacker from bypassing its restrictions at the OS level.

When you start Node.js with --permission, the following capabilities are restricted by default:

Scope Flag Runtime Check
File System Read --allow-fs-read process.permission.has('fs.read', path)
File System Write --allow-fs-write process.permission.has('fs.write', path)
Network --allow-net process.permission.has('net')
Child Process --allow-child-process process.permission.has('child')
Worker Threads --allow-worker process.permission.has('worker')
Native Addons --allow-addons process.permission.has('addons')
WASI --allow-wasi process.permission.has('wasi')
FFI --allow-ffi process.permission.has('ffi')
Inspector Disabled by --permission process.permission.has('inspector')

Starting with --permission

The simplest invocation restricts everything:

$ node --permission index.js

Error: Access to this API has been restricted
    at node:internal/main/run_main_module:23:47 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/home/user/index.js'
}

Even reading the entrypoint file is denied! The model correctly identifies that no read permission was granted. To actually run your application, grant the required permissions explicitly:

$ node --permission \
    --allow-fs-read=/home/user/project \
    --allow-fs-write=/tmp \
    app.js

By default, the entrypoint of your application is automatically included in the allowed file system read list — so app.js will be readable even without an explicit --allow-fs-read for it.

Runtime API: process.permission

When the Permission Model is enabled, a new permission property is added to the process object. It provides two methods for runtime permission management.

process.permission.has(scope, reference)

Checks whether a permission has been granted for a specific scope and optional resource:

process.permission.has('fs.write');                    // true (write was granted)
process.permission.has('fs.write', '/tmp/log.txt'); // true (we allowed /tmp)
process.permission.has('fs.read', '/etc/passwd');   // false (not in allowed paths)
process.permission.has('child');                    // false (child process not allowed)
process.permission.has('net');                      // false (network not allowed)

This is useful for graceful degradation: instead of trying an operation and catching ERR_ACCESS_DENIED, check permission first and provide a clear error or alternative path.

process.permission.drop(scope, reference)

This is the headline feature — an irreversible runtime API that permanently revokes a permission. Once dropped, the permission cannot be re-granted in the same process (there is no permission.grant() method, by design).

const fs = require('node:fs');

// Read config at startup while we still have permission
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');

// Permanently drop read access to /etc/myapp after initialization
process.permission.drop('fs.read', '/etc/myapp');

// This will now throw ERR_ACCESS_DENIED
process.permission.has('fs.read', '/etc/myapp/config.json'); // false

// Drop child process permission entirely
process.permission.drop('child');

// Network scope — drop everything
process.permission.drop('net');

Important Rules for permission.drop()

The Principle of Least Privilege, Applied at Runtime

The real power of permission.drop() is the ability to implement the principle of least privilege over time. A typical pattern:

// Phase 1: Initialization — needs broad access
const config = JSON.parse(fs.readFileSync('/etc/app/config.json', 'utf8'));
const db = require('better-sqlite3')('/var/lib/app/data.db');
process.permission.drop('fs.read', '/etc/app');
process.permission.drop('fs.write', '/var/lib/app');

// Phase 2: Request handling — needs network + limited file access
const server = require('http').createServer((req, res) => {
  if (req.url === '/logs') {
    const log = fs.readFileSync('/var/log/app/request.log', 'utf8');
    res.end(log);
  }
});
server.listen(3000);

// After some time, drop read access to logs too
process.permission.drop('fs.read', '/var/log/app');

// Phase 3: Only network remains — the process can respond but
// cannot read/write any files or spawn child processes.

File System Permissions in Detail

The --allow-fs-read and --allow-fs-write flags accept several forms of arguments:

# Allow all file system reads
--allow-fs-read=*

# Allow reads to /tmp and /home/.gitignore specifically
--allow-fs-read=/tmp/ --allow-fs-read=/home/.gitignore

# Allow writes to /tmp/ folder
--allow-fs-write=/tmp/

# Allow reads matching a wildcard pattern
--allow-fs-read=/home/test*

When the Permission Model initializes, it automatically adds a wildcard (*) if the specified directory exists. For example, if /home/test/files exists, it is treated as /home/test/files/*. If the directory does not exist, the wildcard is not added, and access is limited to the exact path. If you want to allow access to a folder that does not exist yet, explicitly include the wildcard: /my-path/folder-not-yet-exists/*.

Configuration File Support

In addition to command-line flags, permissions can be declared in a Node.js configuration file using the --experimental-config-file flag. Create a file named node.config.json:

{
  "permission": {
    "allow-fs-read": ["./foo", "./bar"],
    "allow-fs-write": ["./tmp"],
    "allow-child-process": true,
    "allow-worker": true,
    "allow-net": true,
    "allow-addons": false,
    "allow-ffi": false
  }
}
$ node --experimental-default-config-file app.js

When the permission namespace is present in the configuration file, Node.js automatically enables the --permission flag — you don't need to pass it separately.

Using the Permission Model with npx

If you're running scripts via npx, you can enable the Permission Model using the --node-options flag:

# Basic — enable permission model
npx --node-options="--permission" package-name

# With file system access for global modules
npx --node-options="--permission --allow-fs-read=$(npm prefix -g)" package-name

# With npx cache access
npx --node-options="--permission --allow-fs-read=$(npm config get cache)" package-name

The --node-options flag sets the NODE_OPTIONS environment variable for all Node.js processes spawned by npx, without affecting the npx process itself.

Security Best Practices

1. Start Deny-All, Then Grant Selectively

Begin with --permission and no --allow-* flags. Run your application, observe which ERR_ACCESS_DENIED errors occur, and grant only the required permissions. This ensures you never accidentally over-permission.

2. Use drop() After Initialization

Many applications need broad permissions at startup (reading config files, opening database connections) but almost none afterwards. Use process.permission.drop() after initialization to permanently reduce your attack surface:

// Initialize application
initializeApp();

// Lock down: drop all file system write, child process, and network
process.permission.drop('fs.write');
process.permission.drop('child');

// Now this process can only read files and respond to requests

3. Graceful Degradation with permission.has()

Before attempting a potentially restricted operation, check permission first:

if (process.permission.has('fs.write', '/tmp/cache')) {
  fs.writeFileSync('/tmp/cache/data.json', JSON.stringify(data));
} else {
  // Fall back to in-memory cache
  inMemoryCache.set(key, data);
}

4. Combine with OS-Level Sandboxing

The Permission Model is not a replacement for OS-level security. Run your Node.js process in a container, use a non-root user, and consider seccomp/AppArmor profiles. The Permission Model layers on top of these — providing defense in depth at the application level where OS tools have no visibility.

Limitations and Known Issues

Be aware of these important constraints:

Before vs After: Permission Model Migration

Here's what a typical Express.js application looks like before and after adding the Permission Model:

Before: No Permission Model

$ node server.js
# The process has full access to:
# - Every file on the filesystem
# - All network interfaces
# - Child process spawning
# - Any native addon
# If compromised, the attacker owns the entire server

After: With Permission Model + Runtime Drop

$ node --permission \
    --allow-fs-read=/etc/app/config.json \
    --allow-fs-read=/var/www \
    --allow-fs-write=/var/log/app \
    --allow-net \
    server.js
const fs = require('node:fs');
const path = require('node:path');

// Phase 1: Read config and initialize
const config = JSON.parse(
  fs.readFileSync('/etc/app/config.json', 'utf8')
);

// Drop config read access — no longer needed
process.permission.drop('fs.read', '/etc/app/config.json');

// Phase 2: Only /var/www (static files) and /var/log/app remain accessible
const express = require('express');
const app = express();

app.use(express.static('/var/www'));
app.post('/api/data', (req, res) => {
  fs.writeFileSync('/var/log/app/requests.log', data);
  res.json({ ok: true });
});

app.listen(3000);

The key difference: after the first process.permission.drop() call, even if the application is compromised, the attacker cannot read /etc/app/config.json to extract secrets — the permission has been irreversibly revoked.

Real-World Use Case: Secure Express.js API Server

Let's put it all together with a complete, production-style example of a secure API server using the Permission Model:

// secure-server.js
const fs = require('node:fs');
const http = require('node:http');
const path = require('node:path');

// === INITIALIZATION PHASE ===

// Load secrets from restricted paths
const dbConfig = JSON.parse(
  fs.readFileSync('/secrets/database.json', 'utf8')
);
const apiKey = fs.readFileSync('/secrets/api-key.txt', 'utf8').trim();

// Drop ALL file system read access — secrets are loaded
process.permission.drop('fs.read');

// Drop ALL file system write access — no files should change
process.permission.drop('fs.write');

// Drop child process spawning — the server never needs it
process.permission.drop('child');

// Drop worker threads — not needed for this service
process.permission.drop('worker');

// === REQUEST HANDLING PHASE ===
// Only network permission remains

const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', permissions: 'locked' }));
    return;
  }

  if (req.url === '/api/data') {
    // Graceful degradation: check permission first
    if (!process.permission.has('fs.read')) {
      // But wait — we already dropped it!
      // This demonstrates that the permission is truly gone
      res.writeHead(403, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'file access not available' }));
      return;
    }
  }

  res.writeHead(200);
  res.end('Hello from locked-down process');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

This pattern — initialize, drop permissions, serve — is the practical embodiment of the principle of least privilege for server-side Node.js applications.

For a broader overview of all the new features in Node.js 26, check my Complete Node.js 26 Guide covering Temporal API, V8 14.6, and Undici 8.

For secure Node.js applications, see my development services

FAQ

What is the Node.js Permission Model?
The Node.js Permission Model is a security mechanism that restricts a Node.js process's access to system resources — file system, network, child processes, worker threads, native addons, WASI, FFI, and the V8 Inspector. When enabled via the --permission flag, all permissions are denied by default and you grant access explicitly using --allow-* flags. It follows a 'seat belt' approach: preventing trusted code from unintentionally accessing resources beyond what was explicitly granted.
How does process.permission.drop() work?
process.permission.drop(scope, reference) is an irreversible runtime API that revokes permissions. Without a reference argument, the entire scope is dropped. With a reference, only the specific resource permission is revoked. You can only drop the exact resource that was explicitly granted — wildcard grants require dropping the entire scope. Dropping a permission does not close already-open resources like file descriptors or network sockets — the application must close those itself.
Did Node.js 26.3 change the Permission Model's stability?
Yes. Node.js 26.3.0 (June 1, 2026) removed the --experimental prefix from the --permission flag, graduating the Permission Model from 'Experimental' to 'Stable' (Stability 2). The API surface is now considered production-ready with backward compatibility guarantees for the lifetime of Node.js 26. The core process.permission.has() and process.permission.drop() APIs remained unchanged during this graduation.
What scopes does the Permission Model control?
The Permission Model controls: FileSystemRead (--allow-fs-read), FileSystemWrite (--allow-fs-write), ChildProcess (--allow-child-process), WorkerThreads (--allow-worker), NativeAddons (--allow-addons), WASI (--allow-wasi), FFI (--allow-ffi), Network (--allow-net), and Inspector (disabled by --permission). Each scope is checked with process.permission.has(scope, reference) and dropped with process.permission.drop(scope, reference).
Can I use the Permission Model with npx?
Yes. Use --node-options to pass permission flags: npx --node-options="--permission" package-name. You'll likely also need file system access: npx --node-options="--permission --allow-fs-read=$(npm prefix -g)" package-name. All standard --allow-* flags work inside --node-options, which sets NODE_OPTIONS for all Node.js processes spawned by npx without affecting the npx process itself.
What are the limitations of the Node.js Permission Model?
Key limitations: (1) The model does not inherit to worker threads. (2) Flags like --env-file and --openssl-config read files before initialization and bypass the model. (3) Native modules, network, child process, worker threads, inspector, WASI, and FFI are all restricted by default. (4) Access via already-open file descriptors bypasses the model. (5) Symbolic links are followed outside allowed paths. (6) process._debugProcess() can signal other Node.js processes regardless of permission state. (7) The node:sqlite module can bypass file system restrictions.
How do I check permissions at runtime?
Use process.permission.has(scope, reference) to check if a permission is still active. For example: process.permission.has('fs.write', '/tmp') returns true if file system write access to /tmp is granted. process.permission.has('child') returns true if child process spawning is allowed. This is useful for graceful degradation — check before attempting operations that may throw ERR_ACCESS_DENIED.

Ready to Secure Your Node.js Application?

The Node.js Permission Model, now stable in Node.js 26.3, brings runtime security to the JavaScript runtime in a way that's practical, composable, and easy to adopt incrementally. The process.permission.drop() API — the ability to irreversibly revoke permissions after initialization — is especially powerful for implementing the principle of least privilege in long-running server processes.

If you're building Node.js applications and want to add proper security hardening, I can help you design a permission strategy for your specific architecture. Get in touch for a free consultation on your project's security posture.

I'm a full-stack developer with 20+ years of experience building and securing Node.js applications — from startups to enterprise platforms. Based in Minsk and working worldwide, let's discuss your project.

Contact

Let's secure your Node.js project

Need help hardening your Node.js application or migrating to Node.js 26 with the Permission Model? I provide free initial consultations.