Skip to main content

Overview

The Node.js Permission Model is a mechanism for restricting access to specific resources during execution. It implements a “seat belt” approach that prevents trusted code from unintentionally changing files or using resources without explicit permission.
node --permission --allow-fs-read=/app --allow-fs-write=/tmp index.js
The Permission Model does not provide security guarantees against malicious code. Malicious code can bypass the permission model. This feature is designed to prevent accidental misuse by trusted code.

Enabling Permissions

Activate the permission model with the --permission flag:
node --permission index.js
When enabled, the following are restricted by default:
  • File system access via fs module
  • Network access
  • Child process spawning
  • Worker threads
  • Native addons
  • WASI
  • Runtime inspector

Permission Types

File System Permissions

Allow File System Read

Grant read access to specific paths:
# Allow all reads
node --permission --allow-fs-read=* index.js

# Allow specific directory
node --permission --allow-fs-read=/home/user/data index.js

# Multiple paths
node --permission --allow-fs-read=/tmp --allow-fs-read=/var/log index.js

# Wildcard patterns
node --permission --allow-fs-read=/home/user/* index.js
Automatic Inclusions: The entry point is automatically included in allowed reads:
# index.js is automatically allowed
node --permission index.js

# Both files are allowed
node --permission -r setup.js index.js

Allow File System Write

Grant write access to specific paths:
# Allow all writes
node --permission --allow-fs-write=* index.js

# Allow specific directory
node --permission --allow-fs-write=/tmp index.js

# Multiple paths
node --permission --allow-fs-write=/tmp --allow-fs-write=/var/log index.js
Example Usage:
// With: node --permission --allow-fs-read=* --allow-fs-write=/tmp index.js
const fs = require('fs');

// Allowed: read from anywhere
const data = fs.readFileSync('/etc/hosts', 'utf8');

// Allowed: write to /tmp
fs.writeFileSync('/tmp/output.txt', 'data');

// Denied: write outside allowed path
try {
  fs.writeFileSync('/etc/hosts', 'data');
} catch (err) {
  console.error('Access denied:', err.code); // ERR_ACCESS_DENIED
}
Wildcard Behavior:
# Match everything starting with pattern
node --permission --allow-fs-read=/home/test* index.js
# Allows: /home/test, /home/test/file, /home/test2

# Directory wildcards
node --permission --allow-fs-read=/home/user/* index.js
# Allows: /home/user/documents/file.txt
If a directory exists, a wildcard (*) is automatically appended. For non-existent directories, explicitly add the wildcard.

Network Permissions

Control network access:
# Allow all network access
node --permission --allow-net index.js
Example:
// With: node --permission --allow-net index.js
const https = require('https');

// Allowed
https.get('https://api.example.com', (res) => {
  console.log('Request successful');
});
Without --allow-net:
// Denied: throws ERR_ACCESS_DENIED
const https = require('https');
https.get('https://api.example.com', (res) => {
  // This will fail
});

Child Process Permissions

Allow spawning child processes:
# Allow child processes
node --permission --allow-child-process index.js
Example:
// With: node --permission --allow-child-process index.js
const { spawn } = require('child_process');

// Allowed
const child = spawn('ls', ['-la']);
child.stdout.on('data', (data) => {
  console.log(data.toString());
});

Worker Thread Permissions

Allow creating worker threads:
# Allow worker threads
node --permission --allow-worker index.js
Example:
// With: node --permission --allow-worker index.js
const { Worker } = require('worker_threads');

// Allowed
const worker = new Worker('./worker.js');

Native Addon Permissions

Allow loading native addons:
# Allow native addons
node --permission --allow-addons index.js
Example:
// With: node --permission --allow-addons index.js
const nativeModule = require('./native-addon.node');

WASI Permissions

Allow WebAssembly System Interface:
# Allow WASI
node --permission --allow-wasi index.js

Runtime API

Check permissions at runtime using process.permission:

process.permission.has(scope[, reference])

Check if permission is granted:
// Check general permission
if (process.permission.has('fs.write')) {
  console.log('File system write access granted');
}

// Check specific path
if (process.permission.has('fs.write', '/tmp/file.txt')) {
  console.log('Can write to /tmp/file.txt');
} else {
  console.log('Cannot write to /tmp/file.txt');
}

// Check read permission
if (process.permission.has('fs.read', '/etc/passwd')) {
  const data = fs.readFileSync('/etc/passwd');
}
Available Scopes:
  • 'fs.read' - File system read
  • 'fs.write' - File system write
  • 'child' - Child process
  • 'worker' - Worker threads
Practical Example:
function safeWriteFile(path, data) {
  if (!process.permission) {
    // No permission model active
    fs.writeFileSync(path, data);
    return;
  }
  
  if (process.permission.has('fs.write', path)) {
    fs.writeFileSync(path, data);
  } else {
    throw new Error(`No permission to write to ${path}`);
  }
}

Configuration File Support

Define permissions in a configuration file: node.config.json:
{
  "permission": {
    "allow-fs-read": ["./src", "./data"],
    "allow-fs-write": ["./tmp", "./logs"],
    "allow-child-process": true,
    "allow-worker": true,
    "allow-net": true,
    "allow-addons": false,
    "allow-wasi": false
  }
}
Usage:
node --experimental-default-config-file app.js
When the permission namespace is present in the config file, the --permission flag is automatically enabled.

Using with npx

Enable permissions when using npx:
# Basic usage
npx --node-options="--permission" package-name

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

# With npm cache access
npx --node-options="--permission --allow-fs-read=$(npm config get cache)" package-name
Example:
# Run a package with limited permissions
npx --node-options="--permission --allow-fs-read=/tmp --allow-net" some-tool

Common Patterns

Development vs Production

Development (permissive):
node --permission \
  --allow-fs-read=* \
  --allow-fs-write=* \
  --allow-net \
  --allow-child-process \
  app.js
Production (restrictive):
node --permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/tmp \
  --allow-net \
  app.js

Web Server Example

# Web server with limited file access
node --permission \
  --allow-fs-read=/app/public \
  --allow-fs-read=/app/views \
  --allow-fs-write=/app/logs \
  --allow-net \
  server.js

CLI Tool Example

# CLI tool that reads config and writes output
node --permission \
  --allow-fs-read=/home/user/.config \
  --allow-fs-write=/home/user/output \
  --allow-child-process \
  cli.js

Data Processing Example

# Data processor with specific paths
node --permission \
  --allow-fs-read=/data/input \
  --allow-fs-write=/data/output \
  --allow-worker \
  process-data.js

Error Handling

Permission errors throw with code ERR_ACCESS_DENIED:
const fs = require('fs');

try {
  fs.writeFileSync('/forbidden/path.txt', 'data');
} catch (err) {
  if (err.code === 'ERR_ACCESS_DENIED') {
    console.error('Permission denied');
    console.error('Permission:', err.permission);
    console.error('Resource:', err.resource);
  }
}
Error Properties:
  • code - Always 'ERR_ACCESS_DENIED'
  • permission - Permission type (e.g., 'FileSystemRead')
  • resource - Resource path or identifier

Constraints and Limitations

The permission model is initialized after Node.js environment setup. Flags like --env-file are not subject to permission rules.

Known Limitations

  1. Symbolic Links
    • Symbolic links are followed, potentially allowing access outside permitted paths
    • Ensure no relative symbolic links exist in permitted directories
  2. Worker Threads
    • Permission model does not inherit to worker threads
    • Each worker operates with full permissions
  3. File Descriptors
    • Using existing file descriptors bypasses the permission model
    • Pre-opened files are not restricted
  4. V8 Flags
    • V8 flags set via v8.setFlagsFromString() bypass permissions
  5. OpenSSL
    • OpenSSL engines cannot be requested at runtime
    • Affects crypto, https, and tls modules
  6. SQLite Extensions
    • Run-time loadable extensions cannot be loaded
    • Affects the sqlite module

Restricted Features

When permission model is enabled:
  • ✗ Native modules (unless --allow-addons)
  • ✗ Network access (unless --allow-net)
  • ✗ Child processes (unless --allow-child-process)
  • ✗ Worker threads (unless --allow-worker)
  • ✗ Inspector protocol
  • ✗ File system access (unless explicitly allowed)
  • ✗ WASI (unless --allow-wasi)

Best Practices

Grant minimal permissions required for your application to function.
Use specific paths instead of wildcards when possible.
Do not rely on the permission model for security against malicious code. It is designed to prevent accidental mistakes.

Principle of Least Privilege

# Bad: too permissive
node --permission --allow-fs-read=* --allow-fs-write=* app.js

# Good: specific paths only
node --permission \
  --allow-fs-read=/app/config \
  --allow-fs-read=/app/data \
  --allow-fs-write=/app/logs \
  app.js

Environment-Specific Configuration

package.json:
{
  "scripts": {
    "start": "node --permission --allow-fs-read=./src --allow-net server.js",
    "dev": "node --permission --allow-fs-read=* --allow-fs-write=* server.js",
    "test": "node --permission --allow-fs-read=./test app.test.js"
  }
}

Programmatic Permission Checks

class SecureFileManager {
  constructor() {
    this.hasPermission = !!process.permission;
  }
  
  canRead(path) {
    if (!this.hasPermission) return true;
    return process.permission.has('fs.read', path);
  }
  
  canWrite(path) {
    if (!this.hasPermission) return true;
    return process.permission.has('fs.write', path);
  }
  
  readFile(path) {
    if (!this.canRead(path)) {
      throw new Error(`No read permission for ${path}`);
    }
    return fs.readFileSync(path);
  }
  
  writeFile(path, data) {
    if (!this.canWrite(path)) {
      throw new Error(`No write permission for ${path}`);
    }
    fs.writeFileSync(path, data);
  }
}

Migration Guide

Step 1: Identify Required Permissions

Run your application and note what resources it accesses:
# Enable debug to see access attempts
NODE_DEBUG=permission node app.js

Step 2: Start with Minimal Permissions

node --permission --allow-fs-read=./config app.js

Step 3: Add Permissions as Needed

node --permission \
  --allow-fs-read=./config \
  --allow-fs-read=./data \
  --allow-fs-write=./logs \
  --allow-net \
  app.js

Step 4: Document Permissions

Create a permissions.md file documenting why each permission is needed.