From b0ebfb1a50b4a8dbe0734137dbf18cccb9f692f3 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. Replace the substring check with proper token parsing that recognizes only actual --permission and --permission-audit flags. Add regression tests for --title=--permission, --conditions=--permission, --trace-event-categories=--permission, and --title=--permission-audit. 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); +}