Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2130,13 +2130,39 @@ changes:
Enable the Permission Model for current process. When enabled, the
following permissions are restricted:

> See also [`--permission-audit`](#--permission-audit) for an audit-only mode
> that logs violations without denying access.

* File System - manageable through
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
* Addons - manageable through [`--allow-addons`][] flag

### `--permission-audit`

<!-- YAML
added: REPLACEME
-->

Enable audit mode for the permission model. When enabled, permission checks
are performed but access is **not** denied — no `ERR_ACCESS_DENIED` error is
thrown. Instead, each permission violation is published through the
`node:diagnostics_channel` module, and execution continues normally.

This flag does not require [`--permission`](#--permission) to be specified. The
`--allow-*` flags are not needed in audit mode, since no
access is denied.

Audit mode is useful for discovering what permissions your application
requires before deploying with [`--permission`](#--permission). See the
[Permission Model][] documentation for the list of diagnostics channel names
and the message format.

If both [`--permission`](#--permission) and `--permission-audit` are specified,
`--permission` takes precedence and the Permission Model runs in enforce mode.

### `--preserve-symlinks`

<!-- YAML
Expand Down Expand Up @@ -3661,6 +3687,7 @@ one is included in the list below.
* `--openssl-legacy-provider`
* `--openssl-shared-config`
* `--pending-deprecation`
* `--permission-audit`
* `--permission`
* `--preserve-symlinks-main`
* `--preserve-symlinks`
Expand Down
97 changes: 95 additions & 2 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ will restrict access to all available permissions.
The available permissions are documented by the [`--permission`][]
flag.

The Permission Model has two operational modes:

* **Enforce mode** (default when using [`--permission`][]): Access is denied and
an `ERR_ACCESS_DENIED` error is thrown for any operation the process has not
been granted permission to perform.
* **Audit mode** (when using [`--permission-audit`][]): Permission checks are
performed and violations are published through the diagnostics channel, but
access is **not** denied. Execution continues normally. This mode is useful
for discovering what permissions your application requires before deploying
with enforce mode.

When starting Node.js with `--permission`,
the ability to access the file system through the `fs` module, spawn processes,
use `node:worker_threads`, use native addons, use WASI, and enable the runtime inspector
Expand All @@ -73,8 +84,8 @@ flag. For WASI, use the [`--allow-wasi`][] flag.
#### Runtime API

When enabling the Permission Model through the [`--permission`][]
flag a new property `permission` is added to the `process` object.
This property contains one function:
or [`--permission-audit`][] flags, a new property `permission` is added to the
`process` object. This property contains the following functions:

##### `permission.has(scope[, reference])`

Expand All @@ -88,6 +99,87 @@ process.permission.has('fs.read'); // true
process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false
```

##### `permission.drop(scope[, reference])`

API call to drop permissions at runtime. This operation is **irreversible**.

When called without a reference, the entire scope is dropped. When called
with a reference, only the permission for that specific resource is revoked.
Dropping a permission only affects future access checks. It does not close or
revoke access to resources that are already open, such as file descriptors,
child processes, or worker threads. Applications are responsible for closing
or terminating those resources when they are no longer needed.

You can only drop the exact resource that was explicitly granted. The
reference passed to `drop()` must match the original grant. If a permission
was granted using a wildcard (`*`), only the entire scope can be dropped
(by calling `drop()` without a reference). If a directory was granted
(e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files
inside it - you must drop the same directory that was originally granted.

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

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

// 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');
```

#### Audit Mode

The [`--permission-audit`][] flag enables audit mode for the Permission Model.
In audit mode, permission checks are performed but access is **not** denied —
no `ERR_ACCESS_DENIED` error is thrown. Instead, each permission violation is
published through the `node:diagnostics_channel` module, allowing the
application to observe and log which operations would be denied under enforce
mode. Execution continues normally.

Audit mode is useful for discovering what permissions your application
requires before deploying with [`--permission`][]. It can also be combined
with the [`--allow-fs-read`][], [`--allow-fs-write`][],
[`--allow-child-process`][], [`--allow-worker`][], [`--allow-addons`][], and
[`--allow-wasi`][] flags to audit a subset of permissions while granting
others.

When a permission check fails in audit mode, a message is published to the
diagnostics channel corresponding to the denied scope. The channel names are:

* `node:permission-model:fs` — File System (read and write)
* `node:permission-model:child` — Child Process
* `node:permission-model:worker` — Worker Threads
* `node:permission-model:inspector` — Inspector
* `node:permission-model:wasi` — WASI
* `node:permission-model:addon` — Native Addons

Each message is an object with the following properties:

* `permission` {string} The name of the denied permission scope.
* `resource` {string} The resource that access was denied to (e.g. a file path).

```js
const diagnostics_channel = require('node:diagnostics_channel');

diagnostics_channel.channel('node:permission-model:fs').subscribe((msg) => {
console.log(`Permission denied: ${msg.permission} on ${msg.resource}`);
});

// Running with --permission-audit, this publishes a diagnostics channel
// message but does not throw
const fs = require('node:fs');
fs.readFileSync('/etc/passwd');
```

If both [`--permission`][] and [`--permission-audit`][] are specified,
`--permission` takes precedence and the Permission Model runs in enforce mode.

#### File System Permissions

The Permission Model, by default, restricts access to the file system through the `node:fs` module.
Expand Down Expand Up @@ -278,6 +370,7 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
[`--allow-fs-write`]: cli.md#--allow-fs-write
[`--allow-wasi`]: cli.md#--allow-wasi
[`--allow-worker`]: cli.md#--allow-worker
[`--permission-audit`]: cli.md#--permission-audit
[`--permission`]: cli.md#--permission
[`npx`]: https://docs.npmjs.com/cli/commands/npx
[`permission.has()`]: process.md#processpermissionhasscope-reference
70 changes: 69 additions & 1 deletion doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -3150,7 +3150,8 @@ added: v20.0.0

* Type: {Object}

This API is available through the [`--permission`][] flag.
This API is available through the [`--permission`][] or
[`--permission-audit`][] flags.

`process.permission` is an object whose methods are used to manage permissions
for the current process. Additional documentation is available in the
Expand All @@ -3171,6 +3172,9 @@ If no reference is provided, a global scope is assumed, for instance,
`process.permission.has('fs.read')` will check if the process has ALL
file system read permissions.

In audit mode ([`--permission-audit`][]), this method still returns the actual
permission status, but denied operations will not throw `ERR_ACCESS_DENIED`.

The reference has a meaning based on the provided scope. For example,
the reference when the scope is File System means files and folders.

Expand All @@ -3189,6 +3193,68 @@ process.permission.has('fs.read', './README.md');
process.permission.has('fs.read');
```

### `process.permission.drop(scope[, reference])`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active Development

* `scope` {string}
* `reference` {string}

Drops the specified permission from the current process. This operation is
**irreversible** — once a permission is dropped, it cannot be restored through
any Node.js API.

In audit mode ([`--permission-audit`][]), dropping a permission takes effect,
but since denied operations do not throw, the impact is limited to changing the
return value of `permission.has()`.

If no reference is provided, the entire scope is dropped. For example,
`process.permission.drop('fs.read')` will revoke ALL file system read
permissions.

When a reference is provided, only the permission for that specific resource
is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')`
will revoke read access to that directory while keeping other read
permissions intact.

**Important:** You can only drop the exact resource that was explicitly
granted. The reference passed to `drop()` must match the original grant:

* If a permission was granted using a wildcard (`*`), such as
`--allow-fs-read=*`, individual paths cannot be dropped - only the entire
scope can be dropped (by calling `drop()` without a reference).
* If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot
drop access to individual files inside it. You must drop the same directory
that was granted. Any remaining grants continue to apply.

The available scopes are the same as [`process.permission.has()`][]:

* `fs` - All File System (drops both read and write)
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `worker` - Worker thread spawning operation
* `inspector` - Inspector operations
* `wasi` - WASI operations
* `addon` - Native addon operations

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

// Read configuration during startup
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');

// Drop read access to the config directory after initialization
process.permission.drop('fs.read', '/etc/myapp');

// This will now throw ERR_ACCESS_DENIED
fs.readFileSync('/etc/myapp/config.json');
```

## `process.pid`

<!-- YAML
Expand Down Expand Up @@ -4568,6 +4634,7 @@ cases:
[`'message'`]: child_process.md#event-message
[`'uncaughtException'`]: #event-uncaughtexception
[`--no-deprecation`]: cli.md#--no-deprecation
[`--permission-audit`]: cli.md#--permission-audit
[`--permission`]: cli.md#--permission
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
[`Buffer`]: buffer.md
Expand Down Expand Up @@ -4598,6 +4665,7 @@ cases:
[`process.hrtime()`]: #processhrtimetime
[`process.hrtime.bigint()`]: #processhrtimebigint
[`process.kill()`]: #processkillpid-signal
[`process.permission.has()`]: #processpermissionhasscope-reference
[`process.setUncaughtExceptionCaptureCallback()`]: #processsetuncaughtexceptioncapturecallbackfn
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[`queueMicrotask()`]: globals.md#queuemicrotaskcallback
Expand Down
15 changes: 15 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,21 @@ Among other uses, this can be used to enable FIPS-compliant crypto if Node.js is
.It Fl -pending-deprecation
Emit pending deprecation warnings.
.
.It Fl -permission-audit
Enable audit mode for the permission model. When enabled, permission checks
are performed but access is \fBnot\fR denied — no \fBERR_ACCESS_DENIED\fR error is
thrown. Instead, each permission violation is published through the
\fBnode:diagnostics_channel\fR module, and execution continues normally.
This flag does not require \fB--permission\fR to be specified. The
\fB--allow-*\fR flags are not needed in audit mode, since no
access is denied.
Audit mode is useful for discovering what permissions your application
requires before deploying with \fB--permission\fR. See the
Permission Model documentation for the list of diagnostics channel names
and the message format.
If both \fB--permission\fR and \fB--permission-audit\fR are specified,
\fB--permission\fR takes precedence and the Permission Model runs in enforce mode.
.
.It Fl -preserve-symlinks
Instructs the module loader to preserve symbolic links when resolving and caching modules other than the main module.
.
Expand Down
12 changes: 12 additions & 0 deletions lib/internal/process/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ module.exports = ObjectFreeze({

return permission.has(scope, reference);
},
drop(scope, reference) {
validateString(scope, 'scope');
if (reference != null) {
if (isBuffer(reference)) {
validateBuffer(reference, 'reference');
} else {
validateString(reference, 'reference');
}
}

permission.drop(scope, reference);
},
availableFlags() {
return [
'--allow-fs-read',
Expand Down
21 changes: 11 additions & 10 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,26 +634,26 @@ function setupDiagnosticsChannel() {
}

function initializePermission() {
const permission = getOptionValue('--permission');
const permission = getOptionValue('--permission') || getOptionValue('--permission-audit');
if (permission) {
process.binding = function binding(_module) {
throw new ERR_ACCESS_DENIED('process.binding');
};
// Guarantee path module isn't monkey-patched to bypass permission model
ObjectFreeze(require('path'));
const { has } = require('internal/process/permission');
const { has, drop } = require('internal/process/permission');
const warnFlags = [
'--allow-addons',
'--allow-child-process',
'--allow-inspector',
'--allow-wasi',
'--allow-worker',
{ flag: '--allow-addons', enabled: true, code: 'PERM0001' },
{ flag: '--allow-child-process', enabled: true, code: 'PERM0002' },
{ flag: '--allow-inspector', enabled: true, code: 'PERM0004' },
{ flag: '--allow-wasi', enabled: true, code: 'PERM0005' },
{ flag: '--allow-worker', enabled: true, code: 'PERM0006' },
];
for (const flag of warnFlags) {
if (getOptionValue(flag)) {
for (const { flag, enabled, code } of warnFlags) {
if (enabled && getOptionValue(flag)) {
process.emitWarning(
`The flag ${flag} must be used with extreme caution. ` +
'It could invalidate the permission model.', 'SecurityWarning');
'It could invalidate the permission model.', 'SecurityWarning', code);
}
}
const warnCommaFlags = [
Expand All @@ -679,6 +679,7 @@ function initializePermission() {
configurable: false,
value: {
has,
drop,
},
});
} else {
Expand Down
5 changes: 4 additions & 1 deletion src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,11 @@ Environment::Environment(IsolateData* isolate_data,
tracing::CastTracedValue(traced_value));
}

if (options_->permission) {
if (options_->permission || options_->permission_audit) {
permission()->EnablePermissions();
if (options_->permission_audit) {
permission()->EnableWarningOnly();
}
// The process shouldn't be able to neither
// spawn/worker nor use addons or enable inspector
// unless explicitly allowed by the user
Expand Down
3 changes: 3 additions & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_url_pattern.h"
#include "permission/permission.h"
#include "util.h"

#include <string>
Expand Down Expand Up @@ -448,6 +449,8 @@ void DLOpen(const FunctionCallbackInfo<Value>& args) {
return THROW_ERR_DLOPEN_DISABLED(
env, "Cannot load native addon because loading addons is disabled.");
}
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kAddon, "");

auto context = env->context();

Expand Down
Loading