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 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
-
Symbolic Links
- Symbolic links are followed, potentially allowing access outside permitted paths
- Ensure no relative symbolic links exist in permitted directories
-
Worker Threads
- Permission model does not inherit to worker threads
- Each worker operates with full permissions
-
File Descriptors
- Using existing file descriptors bypasses the permission model
- Pre-opened files are not restricted
-
V8 Flags
- V8 flags set via
v8.setFlagsFromString() bypass permissions
-
OpenSSL
- OpenSSL engines cannot be requested at runtime
- Affects crypto, https, and tls modules
-
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.