From 63c2e47cb1d4366e98dd14568f92482d13127962 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 18 Jun 2026 08:56:24 +0000 Subject: [PATCH] child_process: fix permission model propagation via NODE_OPTIONS The substring check env[key].indexOf(--permission) !== -1 in copyPermissionModelFlagsToEnv falsely treats unrelated NODE_OPTIONS values like --title=--permission as if the child already has an explicit Permission Model policy. This prevents flag propagation, causing the child to run without process.permission. Signed-off-by: Matteo Collina --- lib/child_process.js | 18 +++- ...n-child-process-inherit-flags-substring.js | 96 +++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-permission-child-process-inherit-flags-substring.js diff --git a/lib/child_process.js b/lib/child_process.js index 824af65556e32b..0e3e04af0d6e32 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -547,10 +547,26 @@ function getPermissionModelFlagsToCopy() { return permissionModelFlagsToCopy; } +function hasPermissionFlagInEnv(nodeOptions) { + // Parse NODE_OPTIONS into individual tokens and check if any token + // is an actual --permission or --permission-audit flag. We use exact + // token matching rather than substring matching to avoid false positives + // when unrelated option values contain '--permission' (e.g., + // --title=--permission). + if (!nodeOptions) return false; + const tokens = nodeOptions.split(/\s+/); + return tokens.some((token) => + token === '--permission' || + token.startsWith('--permission=') || + token === '--permission-audit' || + token.startsWith('--permission-audit='), + ); +} + function copyPermissionModelFlagsToEnv(env, key, args) { // Do not override if permission was already passed to file if (args.includes('--permission') || args.includes('--permission-audit') || - (env[key] && env[key].indexOf('--permission') !== -1)) { + hasPermissionFlagInEnv(env[key])) { return; } diff --git a/test/parallel/test-permission-child-process-inherit-flags-substring.js b/test/parallel/test-permission-child-process-inherit-flags-substring.js new file mode 100644 index 00000000000000..f992a1f3dc8f3b --- /dev/null +++ b/test/parallel/test-permission-child-process-inherit-flags-substring.js @@ -0,0 +1,96 @@ +// Flags: --permission --allow-child-process --allow-fs-read=* --allow-worker +// Tests that NODE_OPTIONS values containing '--permission' as a substring +// in unrelated option values (e.g., --title=--permission) do NOT suppress +// Permission Model flag propagation to child processes. +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} +if (process.config.variables.node_without_node_options) { + common.skip('missing NODE_OPTIONS support'); +} + +const assert = require('assert'); +const childProcess = require('child_process'); + +// Verify that the parent has Permission Model enabled +assert.ok(process.permission.has('child')); +assert.strictEqual(process.env.NODE_OPTIONS, undefined); + +// Test cases: NODE_OPTIONS values that contain '--permission' as a substring +// but are NOT actual permission flags. These should NOT suppress propagation. +const testCases = [ + { name: 'title', nodeOptions: '--title=--permission' }, + { name: 'conditions', nodeOptions: '--conditions=--permission' }, + { name: 'trace-event-categories', nodeOptions: '--trace-event-categories=--permission' }, + { name: 'title-audit', nodeOptions: '--title=--permission-audit' }, +]; + +for (const { name, nodeOptions } of testCases) { + // Spawn a child with the problematic NODE_OPTIONS value. + // Without the fix, the substring check causes propagation to be skipped, + // and the child will not have process.permission. + const { status, stdout } = childProcess.spawnSync( + process.execPath, + [ + '-e', + ` + console.log(typeof process.permission); + console.log(process.permission && process.permission.has("child")); + console.log(process.env.NODE_OPTIONS); + `, + ], + { + env: { + ...process.env, + 'NODE_OPTIONS': nodeOptions, + } + } + ); + + assert.strictEqual(status, 0, `child process for ${name} exited with status ${status}`); + + const [permType, hasChild] = stdout.toString().split('\n'); + + // Verify the child has Permission Model enabled (the bug caused it to be absent) + assert.strictEqual(permType, 'object', `child ${name} should have process.permission object`); + + // Verify the child inherited child permission + assert.strictEqual(hasChild, 'true', `child ${name} should have child permission`); +} + +// Also verify that a child with a real --permission flag in NODE_OPTIONS +// still gets its own flags honored (regression test for existing behavior). +{ + const { status, stdout } = childProcess.spawnSync( + process.execPath, + [ + '-e', + ` + console.log(process.permission.has("fs.write")); + console.log(process.permission.has("fs.read")); + console.log(process.permission.has("child")); + `, + ], + { + env: { + ...process.env, + 'NODE_OPTIONS': '--permission --allow-fs-write=*', + } + } + ); + + assert.strictEqual(status, 0); + const [fsWrite, fsRead, child] = stdout.toString().split('\n'); + assert.strictEqual(fsWrite, 'true'); + assert.strictEqual(fsRead, 'false'); + assert.strictEqual(child, 'false'); +} + +{ + assert.strictEqual(process.env.NODE_OPTIONS, undefined); +}