Skip to content

Commit 29b915a

Browse files
committed
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 <rafael.nunu@hotmail.com>
1 parent d08513d commit 29b915a

7 files changed

Lines changed: 174 additions & 18 deletions

File tree

doc/api/cli.md

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

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

158164
> Stability: 1.1 - Active development
@@ -183,11 +189,15 @@ Error: Access to this API has been restricted
183189
}
184190
```
185191

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

192202
### `--allow-fs-read`
193203

doc/api/permissions.md

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

196196
There are constraints you need to know before using this system:
197197

198-
* The model does not inherit to a child node process or a worker thread.
198+
* The model does not inherit to a worker thread.
199199
* When using the Permission Model the following features will be restricted:
200200
* Native modules
201201
* Network

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
let emittedDEP0190Already = false;
540567
function normalizeSpawnArguments(file, args, options) {
541568
validateString(file, 'file');
@@ -652,7 +679,8 @@ function normalizeSpawnArguments(file, args, options) {
652679
ArrayPrototypeUnshift(args, file);
653680
}
654681

655-
const env = options.env || process.env;
682+
// Shallow copy to guarantee changes won't impact process.env
683+
const env = options.env || { ...process.env };
656684
const envPairs = [];
657685

658686
// process.env.NODE_V8_COVERAGE always propagates, making it possible to
@@ -672,6 +700,10 @@ function normalizeSpawnArguments(file, args, options) {
672700
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
673701
}
674702

703+
if (permission.isEnabled()) {
704+
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
705+
}
706+
675707
let envKeys = [];
676708
// Prototype values are intentionally included.
677709
for (const key in env) {

lib/internal/process/permission.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,15 @@ 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-net',
43+
'--allow-wasi',
44+
'--allow-worker',
45+
];
46+
},
3647
});

lib/internal/process/pre_execution.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -620,16 +620,8 @@ function initializePermission() {
620620
},
621621
});
622622
} else {
623-
const availablePermissionFlags = [
624-
'--allow-fs-read',
625-
'--allow-fs-write',
626-
'--allow-addons',
627-
'--allow-child-process',
628-
'--allow-net',
629-
'--allow-wasi',
630-
'--allow-worker',
631-
];
632-
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
623+
const { availableFlags } = require('internal/process/permission');
624+
ArrayPrototypeForEach(availableFlags(), (flag) => {
633625
const value = getOptionValue(flag);
634626
if (value.length) {
635627
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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
if (!common.hasCrypto) {
12+
common.skip('no crypto');
13+
}
14+
15+
const assert = require('assert');
16+
const childProcess = require('child_process');
17+
18+
{
19+
assert.ok(process.permission.has('child'));
20+
}
21+
22+
{
23+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
24+
}
25+
26+
{
27+
const { status, stdout } = childProcess.spawnSync(process.execPath,
28+
[
29+
'-e',
30+
`
31+
console.log(process.permission.has("fs.write"));
32+
console.log(process.permission.has("fs.read"));
33+
console.log(process.permission.has("child"));
34+
console.log(process.permission.has("net"));
35+
console.log(process.permission.has("worker"));
36+
`,
37+
]
38+
);
39+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
40+
assert.strictEqual(status, 0);
41+
assert.strictEqual(fsWrite, 'false');
42+
assert.strictEqual(fsRead, 'true');
43+
assert.strictEqual(child, 'true');
44+
assert.strictEqual(net, 'false');
45+
assert.strictEqual(worker, 'true');
46+
}
47+
48+
// It should not override when --permission is passed
49+
{
50+
const { status, stdout } = childProcess.spawnSync(
51+
process.execPath,
52+
[
53+
'--permission',
54+
'--allow-fs-write=*',
55+
'-e',
56+
`
57+
console.log(process.permission.has("fs.write"));
58+
console.log(process.permission.has("fs.read"));
59+
console.log(process.permission.has("child"));
60+
console.log(process.permission.has("net"));
61+
console.log(process.permission.has("worker"));
62+
`,
63+
]
64+
);
65+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
66+
assert.strictEqual(status, 0);
67+
assert.strictEqual(fsWrite, 'true');
68+
assert.strictEqual(fsRead, 'false');
69+
assert.strictEqual(child, 'false');
70+
assert.strictEqual(net, 'false');
71+
assert.strictEqual(worker, 'false');
72+
}
73+
74+
// It should not override when NODE_OPTIONS with --permission is passed
75+
{
76+
const { status, stdout } = childProcess.spawnSync(
77+
process.execPath,
78+
[
79+
'-e',
80+
`
81+
console.log(process.permission.has("fs.write"));
82+
console.log(process.permission.has("fs.read"));
83+
console.log(process.permission.has("child"));
84+
console.log(process.permission.has("net"));
85+
console.log(process.permission.has("worker"));
86+
`,
87+
],
88+
{
89+
env: {
90+
'NODE_OPTIONS': '--permission --allow-fs-write=*',
91+
}
92+
}
93+
);
94+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
95+
assert.strictEqual(status, 0);
96+
assert.strictEqual(fsWrite, 'true');
97+
assert.strictEqual(fsRead, 'false');
98+
assert.strictEqual(child, 'false');
99+
assert.strictEqual(net, 'false');
100+
assert.strictEqual(worker, 'false');
101+
}
102+
103+
{
104+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
105+
}

0 commit comments

Comments
 (0)