Skip to content

Commit 6c60f0f

Browse files
RafaelGSSaduh95
authored andcommitted
permission: propagate permission model flags on spawn
Previously, only child_process.fork propagated the exec arguments (execvArgs) to the child process. This commit adds support for spawn and spawnSync to propagate permission model flags — except when they are already provided explicitly via arguments or through NODE_OPTIONS. Signed-off-by: RafaelGSS <[email protected]> PR-URL: #58853 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ulises Gascón <[email protected]>
1 parent c5040ac commit 6c60f0f

File tree

7 files changed

+170
-17
lines changed

7 files changed

+170
-17
lines changed

doc/api/cli.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ Error: Cannot load native addon because loading addons is disabled.
154154

155155
<!-- YAML
156156
added: v20.0.0
157+
changes:
158+
- version: REPLACEME
159+
pr-url: https://github.com/nodejs/node/pull/58853
160+
description: When spawning process with the permission model enabled.
161+
The flags are inherit to the child Node.js process through
162+
NODE_OPTIONS environment variable.
157163
-->
158164

159165
> Stability: 1.1 - Active development
@@ -184,11 +190,15 @@ Error: Access to this API has been restricted
184190
}
185191
```
186192

187-
Unlike `child_process.spawn`, the `child_process.fork` API copies the execution
188-
arguments from the parent process. This means that if you start Node.js with the
189-
Permission Model enabled and include the `--allow-child-process` flag, calling
190-
`child_process.fork()` will propagate all Permission Model flags to the child
191-
process.
193+
The `child_process.fork()` API inherits the execution arguments from the
194+
parent process. This means that if Node.js is started with the Permission
195+
Model enabled and the `--allow-child-process` flag is set, any child process
196+
created using `child_process.fork()` will automatically receive all relevant
197+
Permission Model flags.
198+
199+
This behavior also applies to `child_process.spawn()`, but in that case, the
200+
flags are propagated via the `NODE_OPTIONS` environment variable rather than
201+
directly through the process arguments.
192202

193203
### `--allow-fs-read`
194204

doc/api/permissions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ easy to configure permissions as needed when using `npx`.
190190

191191
There are constraints you need to know before using this system:
192192

193-
* The model does not inherit to a child node process or a worker thread.
193+
* The model does not inherit to a worker thread.
194194
* When using the Permission Model the following features will be restricted:
195195
* Native modules
196196
* Child process

lib/child_process.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ const {
9595

9696
const MAX_BUFFER = 1024 * 1024;
9797

98+
const permission = require('internal/process/permission');
99+
98100
const isZOS = process.platform === 'os390';
99101
let addAbortListener;
100102

@@ -536,6 +538,31 @@ function copyProcessEnvToEnv(env, name, optionEnv) {
536538
}
537539
}
538540

541+
let permissionModelFlagsToCopy;
542+
543+
function getPermissionModelFlagsToCopy() {
544+
if (permissionModelFlagsToCopy === undefined) {
545+
permissionModelFlagsToCopy = [...permission.availableFlags(), '--permission'];
546+
}
547+
return permissionModelFlagsToCopy;
548+
}
549+
550+
function copyPermissionModelFlagsToEnv(env, key, args) {
551+
// Do not override if permission was already passed to file
552+
if (args.includes('--permission') || (env[key] && env[key].indexOf('--permission') !== -1)) {
553+
return;
554+
}
555+
556+
const flagsToCopy = getPermissionModelFlagsToCopy();
557+
for (const arg of process.execArgv) {
558+
for (const flag of flagsToCopy) {
559+
if (arg.startsWith(flag)) {
560+
env[key] = `${env[key] ? env[key] + ' ' + arg : arg}`;
561+
}
562+
}
563+
}
564+
}
565+
539566
function normalizeSpawnArguments(file, args, options) {
540567
validateString(file, 'file');
541568
validateArgumentNullCheck(file, 'file');
@@ -643,7 +670,8 @@ function normalizeSpawnArguments(file, args, options) {
643670
ArrayPrototypeUnshift(args, file);
644671
}
645672

646-
const env = options.env || process.env;
673+
// Shallow copy to guarantee changes won't impact process.env
674+
const env = options.env || { ...process.env };
647675
const envPairs = [];
648676

649677
// process.env.NODE_V8_COVERAGE always propagates, making it possible to
@@ -663,6 +691,10 @@ function normalizeSpawnArguments(file, args, options) {
663691
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
664692
}
665693

694+
if (permission.isEnabled()) {
695+
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
696+
}
697+
666698
let envKeys = [];
667699
// Prototype values are intentionally included.
668700
for (const key in env) {

lib/internal/process/permission.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,14 @@ module.exports = ObjectFreeze({
3333

3434
return permission.has(scope, reference);
3535
},
36+
availableFlags() {
37+
return [
38+
'--allow-fs-read',
39+
'--allow-fs-write',
40+
'--allow-addons',
41+
'--allow-child-process',
42+
'--allow-wasi',
43+
'--allow-worker',
44+
];
45+
},
3646
});

lib/internal/process/pre_execution.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -666,15 +666,8 @@ function initializePermission() {
666666
},
667667
});
668668
} else {
669-
const availablePermissionFlags = [
670-
'--allow-fs-read',
671-
'--allow-fs-write',
672-
'--allow-addons',
673-
'--allow-child-process',
674-
'--allow-wasi',
675-
'--allow-worker',
676-
];
677-
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
669+
const { availableFlags } = require('internal/process/permission');
670+
ArrayPrototypeForEach(availableFlags(), (flag) => {
678671
const value = getOptionValue(flag);
679672
if (value.length) {
680673
throw new ERR_MISSING_OPTION('--permission');

test/parallel/test-permission-allow-child-process-cli.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const assert = require('assert');
1313
const childProcess = require('child_process');
1414
const fs = require('fs');
1515

16+
// Child Process (and fork) should inherit permission model flags
1617
if (process.argv[2] === 'child') {
1718
assert.throws(() => {
1819
fs.writeFileSync(__filename, 'should not write');
@@ -34,7 +35,12 @@ if (process.argv[2] === 'child') {
3435
// doesNotThrow
3536
childProcess.spawnSync(process.execPath, ['--version']);
3637
childProcess.execSync(...common.escapePOSIXShell`"${process.execPath}" --version`);
38+
childProcess.execFileSync(process.execPath, ['--version']);
39+
40+
// Guarantee permission model flags are inherited
3741
const child = childProcess.fork(__filename, ['child']);
3842
child.on('close', common.mustCall());
39-
childProcess.execFileSync(process.execPath, ['--version']);
43+
44+
const { status } = childProcess.spawnSync(process.execPath, [__filename, 'child']);
45+
assert.strictEqual(status, 0);
4046
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Flags: --permission --allow-child-process --allow-fs-read=* --allow-worker
2+
'use strict';
3+
4+
const common = require('../common');
5+
const { isMainThread } = require('worker_threads');
6+
7+
if (!isMainThread) {
8+
common.skip('This test only works on a main thread');
9+
}
10+
11+
const assert = require('assert');
12+
const childProcess = require('child_process');
13+
14+
{
15+
assert.ok(process.permission.has('child'));
16+
}
17+
18+
{
19+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
20+
}
21+
22+
{
23+
const { status, stdout } = childProcess.spawnSync(process.execPath,
24+
[
25+
'-e',
26+
`
27+
console.log(process.permission.has("fs.write"));
28+
console.log(process.permission.has("fs.read"));
29+
console.log(process.permission.has("child"));
30+
console.log(process.permission.has("net"));
31+
console.log(process.permission.has("worker"));
32+
`,
33+
]
34+
);
35+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
36+
assert.strictEqual(status, 0);
37+
assert.strictEqual(fsWrite, 'false');
38+
assert.strictEqual(fsRead, 'true');
39+
assert.strictEqual(child, 'true');
40+
assert.strictEqual(net, 'false');
41+
assert.strictEqual(worker, 'true');
42+
}
43+
44+
// It should not override when --permission is passed
45+
{
46+
const { status, stdout } = childProcess.spawnSync(
47+
process.execPath,
48+
[
49+
'--permission',
50+
'--allow-fs-write=*',
51+
'-e',
52+
`
53+
console.log(process.permission.has("fs.write"));
54+
console.log(process.permission.has("fs.read"));
55+
console.log(process.permission.has("child"));
56+
console.log(process.permission.has("net"));
57+
console.log(process.permission.has("worker"));
58+
`,
59+
]
60+
);
61+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
62+
assert.strictEqual(status, 0);
63+
assert.strictEqual(fsWrite, 'true');
64+
assert.strictEqual(fsRead, 'false');
65+
assert.strictEqual(child, 'false');
66+
assert.strictEqual(net, 'false');
67+
assert.strictEqual(worker, 'false');
68+
}
69+
70+
// It should not override when NODE_OPTIONS with --permission is passed
71+
{
72+
const { status, stdout } = childProcess.spawnSync(
73+
process.execPath,
74+
[
75+
'-e',
76+
`
77+
console.log(process.permission.has("fs.write"));
78+
console.log(process.permission.has("fs.read"));
79+
console.log(process.permission.has("child"));
80+
console.log(process.permission.has("net"));
81+
console.log(process.permission.has("worker"));
82+
`,
83+
],
84+
{
85+
env: {
86+
...process.env,
87+
'NODE_OPTIONS': '--permission --allow-fs-write=*',
88+
}
89+
}
90+
);
91+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
92+
assert.strictEqual(status, 0);
93+
assert.strictEqual(fsWrite, 'true');
94+
assert.strictEqual(fsRead, 'false');
95+
assert.strictEqual(child, 'false');
96+
assert.strictEqual(net, 'false');
97+
assert.strictEqual(worker, 'false');
98+
}
99+
100+
{
101+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
102+
}

0 commit comments

Comments
 (0)