Node.js Permission Model: Sandboxing Your Applications
Use the Node.js permission model to restrict file system, network, and child process access for secure application sandboxing.
What you'll learn
- ✓How the Node.js permission model works
- ✓Restricting file system, network, and child process access
- ✓Running untrusted code safely
- ✓Combining permissions with other security measures
Prerequisites
- •Basic Node.js knowledge
- •Node.js 20 or later installed
- •Understanding of file system operations
The Node.js permission model lets you restrict what your application can do at runtime. You can prevent file system access, block network connections, disable child process spawning, and more. This is especially useful when running third-party code, plugins, or build scripts that you do not fully trust. The permission model was introduced in Node.js 20 and has been refined in each subsequent release.
Enabling the permission model
The permission model is off by default. Enable it with the --experimental-permission flag:
node --experimental-permission app.js
With this flag, all permissions are denied by default. Your application cannot read files, write files, spawn child processes, or use worker threads unless you explicitly grant those permissions.
// app.js — this will throw an ERR_ACCESS_DENIED error
import { readFile } from 'node:fs/promises';
const data = await readFile('/etc/hosts', 'utf-8');
console.log(data);
node --experimental-permission app.js
# Error: Access to this API has been restricted
# code: 'ERR_ACCESS_DENIED'
# permission: 'FileSystemRead'
Granting file system permissions
Use --allow-fs-read and --allow-fs-write to grant access to specific paths:
# Allow reading from a specific directory
node --experimental-permission --allow-fs-read=/app/data app.js
# Allow reading from multiple paths
node --experimental-permission --allow-fs-read=/app/data --allow-fs-read=/app/config app.js
# Allow writing to a specific directory
node --experimental-permission --allow-fs-write=/app/output app.js
# Allow all file system reads (use sparingly)
node --experimental-permission --allow-fs-read=* app.js
Paths are resolved to absolute paths, and access is granted recursively to subdirectories:
import { readFile, writeFile } from 'node:fs/promises';
// Allowed — within /app/data
const config = await readFile('/app/data/config.json', 'utf-8');
// Blocked — outside permitted paths
const secrets = await readFile('/etc/shadow', 'utf-8'); // ERR_ACCESS_DENIED
// Allowed — within /app/output
await writeFile('/app/output/result.json', '{}');
// Blocked — cannot write to /app/data (only read was granted)
await writeFile('/app/data/config.json', '{}'); // ERR_ACCESS_DENIED
Restricting child processes and workers
Prevent your application from spawning child processes or creating worker threads:
# Allow child process spawning
node --experimental-permission --allow-child-process app.js
# Allow worker threads
node --experimental-permission --allow-worker app.js
Without these flags, any attempt to use child_process, cluster, or worker_threads will throw:
import { exec } from 'node:child_process';
// This throws ERR_ACCESS_DENIED without --allow-child-process
exec('rm -rf /', (err, stdout) => {
console.log(stdout);
});
This is critical for security. A malicious dependency cannot execute arbitrary shell commands if child process access is denied.
Checking permissions at runtime
You can check whether a permission is granted before attempting an operation:
import { permission } from 'node:process';
if (permission.has('fs.read', '/app/data')) {
console.log('Can read /app/data');
}
if (permission.has('fs.write', '/app/output')) {
console.log('Can write to /app/output');
}
if (permission.has('child')) {
console.log('Can spawn child processes');
}
if (permission.has('worker')) {
console.log('Can create worker threads');
}
This is useful for libraries that want to provide helpful error messages instead of letting the runtime throw.
Practical example: secure plugin runner
Imagine you are building an application that loads and runs user-submitted plugins. You want to ensure plugins cannot access the file system outside their sandbox or spawn processes.
// plugin-runner.js
import { Worker } from 'node:worker_threads';
import { join } from 'node:path';
function runPlugin(pluginPath, sandboxDir) {
return new Promise((resolve, reject) => {
const worker = new Worker(pluginPath, {
execArgv: [
'--experimental-permission',
`--allow-fs-read=${sandboxDir}`,
`--allow-fs-write=${sandboxDir}`,
// No --allow-child-process
// No --allow-worker (prevent nested workers)
],
workerData: { sandboxDir },
});
worker.on('message', resolve);
worker.on('error', reject);
// Kill the worker after 30 seconds
setTimeout(() => {
worker.terminate();
reject(new Error('Plugin timed out'));
}, 30_000);
});
}
const result = await runPlugin(
'./plugins/user-plugin.js',
'/app/sandbox/user123'
);
The plugin can read and write files within its sandbox directory but cannot access anything else on the system.
Combining with policy files
For more granular control, combine the permission model with Node.js policy files. A policy file specifies which modules can be loaded and their integrity hashes:
{
"resources": {
"./app.js": {
"integrity": "sha384-abc123...",
"dependencies": {
"node:fs": true,
"node:path": true,
"./lib/utils.js": true
}
},
"./lib/utils.js": {
"integrity": "sha384-def456...",
"dependencies": {
"node:crypto": true
}
}
}
}
node --experimental-policy=policy.json --experimental-permission --allow-fs-read=/app app.js
This ensures that only verified modules are loaded, and those modules can only access permitted resources.
Security layers in practice
The permission model is one layer in a defense-in-depth strategy:
// 1. Permission model — restrict OS-level access
// node --experimental-permission --allow-fs-read=/app/data
// 2. Input validation — never trust user input
function processUpload(filePath) {
const resolved = path.resolve(filePath);
// Prevent path traversal
if (!resolved.startsWith('/app/uploads/')) {
throw new Error('Invalid file path');
}
return readFile(resolved);
}
// 3. Principle of least privilege
// Only grant the minimum permissions needed
// Separate read and write access
// Deny child process access unless required
Limitations to be aware of
The permission model has some current limitations:
- Native addons: C++ addons loaded via
requireorprocess.dlopenbypass the permission model. They have full OS-level access. - Environment variables: The permission model does not restrict access to
process.env. A malicious module can still read environment variables containing secrets. - Network access: As of Node.js 22, network permission restrictions are still being developed. You cannot yet restrict which hosts your application can connect to using only the permission model.
- Performance: There is a small overhead for permission checks on every file system operation. For most applications this is negligible.
Recommended production setup
node \
--experimental-permission \
--allow-fs-read=/app \
--allow-fs-write=/app/data \
--allow-fs-write=/app/logs \
--allow-fs-write=/tmp \
app.js
This setup allows your application to read its own code and configuration, write to data and log directories, and use /tmp for temporary files. Child processes and worker threads are denied by default. Adjust based on your application’s actual needs.
Key takeaways
The Node.js permission model brings sandboxing to the runtime level. Enable it with --experimental-permission and explicitly grant only the access your application needs. It is particularly valuable when running third-party code, build scripts, or plugins. Combine it with input validation, path sanitization, and the principle of least privilege for a robust security posture. Watch for the network permission restrictions coming in future Node.js releases.
Related articles
- Django Django Permissions and Authorization
Move beyond is_authenticated. Learn how to model groups, object-level permissions, and DRF permission classes cleanly.
- Linux Linux File Permissions: A chmod and chown Deep Dive
Understand the Linux permission model from user/group/other to setuid and sticky bits, with practical chmod and chown patterns you can use today.
- Linux Linux File Permissions Explained
Read every rwx string at a glance, change permissions with chmod (symbolic and numeric), change ownership with chown, and understand when to use sudo. With runnable examples.
- Node.js Node.js vs Deno vs Bun: JavaScript Runtimes Compared
Compare Node.js, Deno, and Bun on performance, security, compatibility, and developer experience. Choose the right JavaScript runtime for 2026.