Commit 882adc580cf for nodejs
commit 882adc580cfee14536423bfffe67b23b860b3525
Author: Rafael Gonzaga <rafael.nunu@hotmail.com>
Date: Thu Oct 8 17:58:31 2026 -0300
child_process: propagate config-file permission flags to children
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs/node/pull/66559
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
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');