From 839cfb2ba39f2645c7a192a34c3158a53e80a3db Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 17 Feb 2026 14:40:47 -0300 Subject: [PATCH 1/8] src,permission: add --permission-audit Add --permission-audit flag that enables the permission model in warning-only mode. Instead of throwing ERR_ACCESS_DENIED, it emits a message via diagnostics channel and allows the operation to continue. Publish permission check results to per-scope diagnostics channels (e.g., node:permission-model:fs) so users can observe permission decisions at runtime via diagnostics_channel. Refs: https://github.com/nodejs/node/issues/59935 --- doc/api/cli.md | 11 +++ doc/node.1 | 5 + lib/internal/process/pre_execution.js | 2 +- src/env.cc | 5 +- src/node_options.cc | 5 + src/node_options.h | 1 + src/permission/permission.cc | 91 ++++++++++++++++++- src/permission/permission.h | 26 ++++-- .../test-permission-diagnostics-channel.js | 29 ++++++ 9 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 test/parallel/test-permission-diagnostics-channel.js diff --git a/doc/api/cli.md b/doc/api/cli.md index ae0f81b5ec53..9880ff838275 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2137,6 +2137,16 @@ following permissions are restricted: * WASI - manageable through [`--allow-wasi`][] flag * Addons - manageable through [`--allow-addons`][] flag +### `--permission-audit` + + + +Enable audit only for the permission model. When enabled, permission checks +are performed but access is not denied. Instead, a warning is emitted for +each permission violation via diagnostics channel. + ### `--preserve-symlinks` + +> 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. + +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` -Enable audit only for the permission model. When enabled, permission checks -are performed but access is not denied. Instead, a warning is emitted for -each permission violation via diagnostics channel. +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` diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 29e4a068f416..673d38541dd1 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -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 @@ -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 the following functions: +or [`--permission-audit`][] flags, a new property `permission` is added to the +`process` object. This property contains the following functions: ##### `permission.has(scope[, reference])` @@ -122,6 +133,53 @@ process.permission.has('fs.read', '/etc/myapp/config.json'); // false 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. @@ -312,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 diff --git a/doc/api/process.md b/doc/api/process.md index dfe2347d58ed..8f3ea53756c1 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -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 @@ -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. @@ -3204,6 +3208,10 @@ 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. @@ -4626,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 diff --git a/doc/node.1 b/doc/node.1 index 98e80d348dec..aeb2c0eb27c6 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -469,9 +469,19 @@ Among other uses, this can be used to enable FIPS-compliant crypto if Node.js is Emit pending deprecation warnings. . .It Fl -permission-audit -Enable audit only for the permission model. When enabled, permission checks -are performed but access is not denied. Instead, a warning is emitted for -each permission violation via diagnostics channel. +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. From 499c38aeeda3c58827a1af8671845aec13341fbb Mon Sep 17 00:00:00 2001 From: David Evans Date: Sun, 2 Aug 2026 14:54:33 +0100 Subject: [PATCH 8/8] permission: add unique warning codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds unique warning codes of the form PERM0000 for all permissions related SecurityWarnings, so that they can be individually silenced if required. Fixes: https://github.com/nodejs/node/issues/59818 Signed-off-by: David Evans PR-URL: https://github.com/nodejs/node/pull/64414 Reviewed-By: Rafael Gonzaga Reviewed-By: Ulises Gascón Reviewed-By: James M Snell --- lib/internal/process/pre_execution.js | 16 ++++++++-------- test/parallel/test-permission-warning-flags.js | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 63c1b718b459..a7ff17a6467f 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -643,17 +643,17 @@ function initializePermission() { ObjectFreeze(require('path')); 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 = [ diff --git a/test/parallel/test-permission-warning-flags.js b/test/parallel/test-permission-warning-flags.js index 450597942c69..eeb00ecd3517 100644 --- a/test/parallel/test-permission-warning-flags.js +++ b/test/parallel/test-permission-warning-flags.js @@ -21,6 +21,20 @@ for (const flag of warnFlags) { ] ); - assert.match(stderr.toString(), new RegExp(`SecurityWarning: The flag ${RegExp.escape(flag)} must be used with extreme caution`)); + assert.match(stderr.toString(), new RegExp(`\\[PERM\\d{4}\\] SecurityWarning: The flag ${RegExp.escape(flag)} must be used with extreme caution`)); assert.strictEqual(status, 0); } + +const { status, stderr } = spawnSync( + process.execPath, + [ + '--permission', '--allow-child-process', '--allow-wasi', '--disable-warning=PERM0002', '-e', + 'setTimeout(() => {}, 1)', + ] +); + +// Disabled warning does not appear +assert.doesNotMatch(stderr.toString(), new RegExp(`The flag --allow-child-process must be used with extreme caution`)); +// But non-disabled warnings still appear +assert.match(stderr.toString(), new RegExp(`The flag --allow-wasi must be used with extreme caution`)); +assert.strictEqual(status, 0);