From e1bc1805b35a9033c8d5b21ea429505941360440 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 6 Oct 2026 08:03:08 -0300 Subject: [PATCH 1/2] child_process: propagate config-file permission flags to children Signed-off-by: RafaelGSS --- lib/child_process.js | 18 ++++++++++ .../permission/child-process-inherit-test.js | 16 +++++++++ .../config-child-inherit-allow-only.json | 9 +++++ .../permission/config-child-inherit.json | 10 ++++++ test/parallel/test-permission-config-file.mjs | 34 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 test/fixtures/permission/child-process-inherit-test.js create mode 100644 test/fixtures/permission/config-child-inherit-allow-only.json create mode 100644 test/fixtures/permission/config-child-inherit.json diff --git a/lib/child_process.js b/lib/child_process.js index 2d7456346495..f44ae3b23100 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -75,6 +75,7 @@ const { } = require('internal/errors'); const { clearTimeout, setTimeout } = require('timers'); const { getValidatedPath } = require('internal/fs/utils'); +const { getOptionValue } = require('internal/options'); const { validateAbortSignal, validateArray, @@ -579,17 +580,34 @@ function copyPermissionModelFlagsToEnv(env, key, args) { const allowAllEnv = !permission.isAuditMode(); const flagsToCopy = getPermissionModelFlagsToCopy(); + const copiedFlags = new SafeSet(); for (const arg of process.execArgv) { if (allowAllEnv && arg.startsWith('--allow-env')) { continue; } for (const flag of flagsToCopy) { if (arg.startsWith(flag)) { + copiedFlags.add(flag); env[key] = `${env[key] ? env[key] + ' ' + arg : arg}`; } } } + // Flags set through --config-file are not part of process.execArgv. + for (const flag of flagsToCopy) { + if (copiedFlags.has(flag) || (allowAllEnv && flag === '--allow-env')) { + continue; + } + const value = getOptionValue(flag); + if (value === true) { + env[key] = `${env[key] ? env[key] + ' ' : ''}${flag}`; + } else if (ArrayIsArray(value)) { + for (const item of value) { + env[key] = `${env[key] ? env[key] + ' ' : ''}${flag}=${item}`; + } + } + } + if (allowAllEnv) { env[key] = `${env[key] ? env[key] + ' ' : ''}--allow-env=*`; } diff --git a/test/fixtures/permission/child-process-inherit-test.js b/test/fixtures/permission/child-process-inherit-test.js new file mode 100644 index 000000000000..2a095b7d1424 --- /dev/null +++ b/test/fixtures/permission/child-process-inherit-test.js @@ -0,0 +1,16 @@ +const { spawnSync } = require('child_process'); +const { status, stdout, stderr } = spawnSync(process.execPath, [ + '-p', + `JSON.stringify([ + typeof process.permission, + process.permission.has("fs.read"), + process.permission.has("fs.write"), + process.permission.has("child"), + process.permission.has("worker"), + ])`, +]); +console.log(JSON.stringify({ + status, + stdout: stdout.toString().trim(), + stderr: stderr.toString(), +})); diff --git a/test/fixtures/permission/config-child-inherit-allow-only.json b/test/fixtures/permission/config-child-inherit-allow-only.json new file mode 100644 index 000000000000..7f26f3d672ee --- /dev/null +++ b/test/fixtures/permission/config-child-inherit-allow-only.json @@ -0,0 +1,9 @@ +{ + "permission": { + "allow-child-process": true, + "allow-worker": true, + "allow-fs-read": [ + "*" + ] + } +} diff --git a/test/fixtures/permission/config-child-inherit.json b/test/fixtures/permission/config-child-inherit.json new file mode 100644 index 000000000000..832ab206ebd9 --- /dev/null +++ b/test/fixtures/permission/config-child-inherit.json @@ -0,0 +1,10 @@ +{ + "permission": { + "permission": true, + "allow-child-process": true, + "allow-worker": true, + "allow-fs-read": [ + "*" + ] + } +} diff --git a/test/parallel/test-permission-config-file.mjs b/test/parallel/test-permission-config-file.mjs index 6b01f3741f31..c27b1038e71c 100644 --- a/test/parallel/test-permission-config-file.mjs +++ b/test/parallel/test-permission-config-file.mjs @@ -64,6 +64,40 @@ describe('Permission model config file support', () => { } }); + it('should propagate config file permissions to child processes', { + skip: process.config.variables.node_without_node_options && 'missing NODE_OPTIONS support', + }, async () => { + const childTestPath = fixtures.path('permission/child-process-inherit-test.js'); + const expected = JSON.stringify(['object', true, false, true, true]); + + // --permission set in the config file + { + const configPath = fixtures.path('permission/config-child-inherit.json'); + const result = await spawnPromisified(process.execPath, [ + `--config-file=${configPath}`, + childTestPath, + ]); + assert.strictEqual(result.code, 0, result.stderr); + const child = JSON.parse(result.stdout); + assert.strictEqual(child.status, 0, child.stderr); + assert.strictEqual(child.stdout, expected); + } + + // --permission set in the command line + { + const configPath = fixtures.path('permission/config-child-inherit-allow-only.json'); + const result = await spawnPromisified(process.execPath, [ + '--permission', + `--config-file=${configPath}`, + childTestPath, + ]); + assert.strictEqual(result.code, 0, result.stderr); + const child = JSON.parse(result.stdout); + assert.strictEqual(child.status, 0, child.stderr); + assert.strictEqual(child.stdout, expected); + } + }); + it('should load network and inspector permissions from config file', async () => { const configPath = fixtures.path('permission/config-net-inspector.json'); const readOnlyConfigPath = fixtures.path('permission/config-fs-read-only.json'); From d123050b02b8b352479f941c8aed001aaa05cf56 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 6 Oct 2026 10:06:17 -0300 Subject: [PATCH 2/2] chore: apply remaining changes --- lib/child_process.js | 6 +++++- test/fixtures/permission/config-child-inherit.json | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/child_process.js b/lib/child_process.js index f44ae3b23100..6b411f43f074 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -38,6 +38,7 @@ const { ObjectPrototypeHasOwnProperty, PromiseWithResolvers, RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, SafeSet, StringPrototypeIncludes, StringPrototypeIndexOf, @@ -603,7 +604,10 @@ function copyPermissionModelFlagsToEnv(env, key, args) { env[key] = `${env[key] ? env[key] + ' ' : ''}${flag}`; } else if (ArrayIsArray(value)) { for (const item of value) { - env[key] = `${env[key] ? env[key] + ' ' : ''}${flag}=${item}`; + // Values may contain spaces or quotes (e.g. the implicitly allowed + // entry point), so quote them as NODE_OPTIONS expects. + const quoted = RegExpPrototypeSymbolReplace(/["\\]/g, item, '\\$&'); + env[key] = `${env[key] ? env[key] + ' ' : ''}"${flag}=${quoted}"`; } } } diff --git a/test/fixtures/permission/config-child-inherit.json b/test/fixtures/permission/config-child-inherit.json index 832ab206ebd9..c4d4f1a25152 100644 --- a/test/fixtures/permission/config-child-inherit.json +++ b/test/fixtures/permission/config-child-inherit.json @@ -4,7 +4,8 @@ "allow-child-process": true, "allow-worker": true, "allow-fs-read": [ - "*" + "*", + "/nonexistent/dir with \"quotes\" and \\backslash" ] } }