diff --git a/lib/child_process.js b/lib/child_process.js index 2d745634649..6b411f43f07 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -38,6 +38,7 @@ const { ObjectPrototypeHasOwnProperty, PromiseWithResolvers, RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, SafeSet, StringPrototypeIncludes, StringPrototypeIndexOf, @@ -75,6 +76,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 +581,37 @@ 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) { + // 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}"`; + } + } + } + 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 00000000000..2a095b7d142 --- /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 00000000000..7f26f3d672e --- /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 00000000000..c4d4f1a2515 --- /dev/null +++ b/test/fixtures/permission/config-child-inherit.json @@ -0,0 +1,11 @@ +{ + "permission": { + "permission": true, + "allow-child-process": true, + "allow-worker": true, + "allow-fs-read": [ + "*", + "/nonexistent/dir with \"quotes\" and \\backslash" + ] + } +} diff --git a/test/parallel/test-permission-config-file.mjs b/test/parallel/test-permission-config-file.mjs index 6b01f3741f3..c27b1038e71 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');