Skip to content

Commit 00c2225

Browse files
authored
src,process: add permission model
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: #44004 Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 42be7f6 commit 00c2225

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3246
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Call fs.readFile with permission system enabled
2+
// over and over again really fast.
3+
// Then see how many times it got called.
4+
'use strict';
5+
6+
const path = require('path');
7+
const common = require('../common.js');
8+
const fs = require('fs');
9+
const assert = require('assert');
10+
11+
const tmpdir = require('../../test/common/tmpdir');
12+
tmpdir.refresh();
13+
const filename = path.resolve(tmpdir.path,
14+
`.removeme-benchmark-garbage-${process.pid}`);
15+
16+
const bench = common.createBenchmark(main, {
17+
duration: [5],
18+
encoding: ['', 'utf-8'],
19+
len: [1024, 16 * 1024 * 1024],
20+
concurrent: [1, 10],
21+
}, {
22+
flags: ['--experimental-permission', '--allow-fs-read=*', '--allow-fs-write=*'],
23+
});
24+
25+
function main({ len, duration, concurrent, encoding }) {
26+
try {
27+
fs.unlinkSync(filename);
28+
} catch {
29+
// Continue regardless of error.
30+
}
31+
let data = Buffer.alloc(len, 'x');
32+
fs.writeFileSync(filename, data);
33+
data = null;
34+
35+
let reads = 0;
36+
let benchEnded = false;
37+
bench.start();
38+
setTimeout(() => {
39+
benchEnded = true;
40+
bench.end(reads);
41+
try {
42+
fs.unlinkSync(filename);
43+
} catch {
44+
// Continue regardless of error.
45+
}
46+
process.exit(0);
47+
}, duration * 1000);
48+
49+
function read() {
50+
fs.readFile(filename, encoding, afterRead);
51+
}
52+
53+
function afterRead(er, data) {
54+
if (er) {
55+
if (er.code === 'ENOENT') {
56+
// Only OK if unlinked by the timer from main.
57+
assert.ok(benchEnded);
58+
return;
59+
}
60+
throw er;
61+
}
62+
63+
if (data.length !== len)
64+
throw new Error('wrong number of bytes returned');
65+
66+
reads++;
67+
if (!benchEnded)
68+
read();
69+
}
70+
71+
while (concurrent--) read();
72+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
const common = require('../common.js');
3+
4+
const configs = {
5+
n: [1e5],
6+
concurrent: [1, 10],
7+
};
8+
9+
const options = { flags: ['--experimental-permission'] };
10+
11+
const bench = common.createBenchmark(main, configs, options);
12+
13+
async function main(conf) {
14+
bench.start();
15+
for (let i = 0; i < conf.n; i++) {
16+
process.permission.deny('fs.read', ['/home/example-file-' + i]);
17+
}
18+
bench.end(conf.n);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
const common = require('../common.js');
3+
const fs = require('fs/promises');
4+
const path = require('path');
5+
6+
const configs = {
7+
n: [1e5],
8+
concurrent: [1, 10],
9+
};
10+
11+
const rootPath = path.resolve(__dirname, '../../..');
12+
13+
const options = {
14+
flags: [
15+
'--experimental-permission',
16+
`--allow-fs-read=${rootPath}`,
17+
],
18+
};
19+
20+
const bench = common.createBenchmark(main, configs, options);
21+
22+
const recursivelyDenyFiles = async (dir) => {
23+
const files = await fs.readdir(dir, { withFileTypes: true });
24+
for (const file of files) {
25+
if (file.isDirectory()) {
26+
await recursivelyDenyFiles(path.join(dir, file.name));
27+
} else if (file.isFile()) {
28+
process.permission.deny('fs.read', [path.join(dir, file.name)]);
29+
}
30+
}
31+
};
32+
33+
async function main(conf) {
34+
const benchmarkDir = path.join(__dirname, '../..');
35+
// Get all the benchmark files and deny access to it
36+
await recursivelyDenyFiles(benchmarkDir);
37+
38+
bench.start();
39+
40+
for (let i = 0; i < conf.n; i++) {
41+
// Valid file in a sequence of denied files
42+
process.permission.has('fs.read', benchmarkDir + '/valid-file');
43+
// Denied file
44+
process.permission.has('fs.read', __filename);
45+
// Valid file a granted directory
46+
process.permission.has('fs.read', '/tmp/example');
47+
}
48+
49+
bench.end(conf.n);
50+
}

doc/api/cli.md

+169
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,154 @@ If this flag is passed, the behavior can still be set to not abort through
100100
[`process.setUncaughtExceptionCaptureCallback()`][] (and through usage of the
101101
`node:domain` module that uses it).
102102

103+
### `--allow-child-process`
104+
105+
<!-- YAML
106+
added: REPLACEME
107+
-->
108+
109+
> Stability: 1 - Experimental
110+
111+
When using the [Permission Model][], the process will not be able to spawn any
112+
child process by default.
113+
Attempts to do so will throw an `ERR_ACCESS_DENIED` unless the
114+
user explicitly passes the `--allow-child-process` flag when starting Node.js.
115+
116+
Example:
117+
118+
```js
119+
const childProcess = require('node:child_process');
120+
// Attempt to bypass the permission
121+
childProcess.spawn('node', ['-e', 'require("fs").writeFileSync("/new-file", "example")']);
122+
```
123+
124+
```console
125+
$ node --experimental-permission --allow-fs-read=* index.js
126+
node:internal/child_process:388
127+
const err = this._handle.spawn(options);
128+
^
129+
Error: Access to this API has been restricted
130+
at ChildProcess.spawn (node:internal/child_process:388:28)
131+
at Object.spawn (node:child_process:723:9)
132+
at Object.<anonymous> (/home/index.js:3:14)
133+
at Module._compile (node:internal/modules/cjs/loader:1120:14)
134+
at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
135+
at Module.load (node:internal/modules/cjs/loader:998:32)
136+
at Module._load (node:internal/modules/cjs/loader:839:12)
137+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
138+
at node:internal/main/run_main_module:17:47 {
139+
code: 'ERR_ACCESS_DENIED',
140+
permission: 'ChildProcess'
141+
}
142+
```
143+
144+
### `--allow-fs-read`
145+
146+
<!-- YAML
147+
added: REPLACEME
148+
-->
149+
150+
> Stability: 1 - Experimental
151+
152+
This flag configures file system read permissions using
153+
the [Permission Model][].
154+
155+
The valid arguments for the `--allow-fs-read` flag are:
156+
157+
* `*` - To allow the `FileSystemRead` operations.
158+
* Paths delimited by comma (,) to manage `FileSystemRead` (reading) operations.
159+
160+
Examples can be found in the [File System Permissions][] documentation.
161+
162+
Relative paths are NOT yet supported by the CLI flag.
163+
164+
The initializer module also needs to be allowed. Consider the following example:
165+
166+
```console
167+
$ node --experimental-permission t.js
168+
node:internal/modules/cjs/loader:162
169+
const result = internalModuleStat(filename);
170+
^
171+
172+
Error: Access to this API has been restricted
173+
at stat (node:internal/modules/cjs/loader:162:18)
174+
at Module._findPath (node:internal/modules/cjs/loader:640:16)
175+
at resolveMainPath (node:internal/modules/run_main:15:25)
176+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:53:24)
177+
at node:internal/main/run_main_module:23:47 {
178+
code: 'ERR_ACCESS_DENIED',
179+
permission: 'FileSystemRead',
180+
resource: '/Users/rafaelgss/repos/os/node/t.js'
181+
}
182+
```
183+
184+
The process needs to have access to the `index.js` module:
185+
186+
```console
187+
$ node --experimental-permission --allow-fs-read=/path/to/index.js index.js
188+
```
189+
190+
### `--allow-fs-write`
191+
192+
<!-- YAML
193+
added: REPLACEME
194+
-->
195+
196+
> Stability: 1 - Experimental
197+
198+
This flag configures file system write permissions using
199+
the [Permission Model][].
200+
201+
The valid arguments for the `--allow-fs-write` flag are:
202+
203+
* `*` - To allow the `FileSystemWrite` operations.
204+
* Paths delimited by comma (,) to manage `FileSystemWrite` (writing) operations.
205+
206+
Examples can be found in the [File System Permissions][] documentation.
207+
208+
Relative paths are NOT supported through the CLI flag.
209+
210+
### `--allow-worker`
211+
212+
<!-- YAML
213+
added: REPLACEME
214+
-->
215+
216+
> Stability: 1 - Experimental
217+
218+
When using the [Permission Model][], the process will not be able to create any
219+
worker threads by default.
220+
For security reasons, the call will throw an `ERR_ACCESS_DENIED` unless the
221+
user explicitly pass the flag `--allow-worker` in the main Node.js process.
222+
223+
Example:
224+
225+
```js
226+
const { Worker } = require('node:worker_threads');
227+
// Attempt to bypass the permission
228+
new Worker(__filename);
229+
```
230+
231+
```console
232+
$ node --experimental-permission --allow-fs-read=* index.js
233+
node:internal/worker:188
234+
this[kHandle] = new WorkerImpl(url,
235+
^
236+
237+
Error: Access to this API has been restricted
238+
at new Worker (node:internal/worker:188:21)
239+
at Object.<anonymous> (/home/index.js.js:3:1)
240+
at Module._compile (node:internal/modules/cjs/loader:1120:14)
241+
at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
242+
at Module.load (node:internal/modules/cjs/loader:998:32)
243+
at Module._load (node:internal/modules/cjs/loader:839:12)
244+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
245+
at node:internal/main/run_main_module:17:47 {
246+
code: 'ERR_ACCESS_DENIED',
247+
permission: 'WorkerThreads'
248+
}
249+
```
250+
103251
### `--build-snapshot`
104252

105253
<!-- YAML
@@ -386,6 +534,20 @@ added:
386534
387535
Enable experimental support for the `https:` protocol in `import` specifiers.
388536

537+
### `--experimental-permission`
538+
539+
<!-- YAML
540+
added: REPLACEME
541+
-->
542+
543+
Enable the Permission Model for current process. When enabled, the
544+
following permissions are restricted:
545+
546+
* File System - manageable through
547+
\[`--allow-fs-read`]\[],\[`allow-fs-write`]\[] flags
548+
* Child Process - manageable through \[`--allow-child-process`]\[] flag
549+
* Worker Threads - manageable through \[`--allow-worker`]\[] flag
550+
389551
### `--experimental-policy`
390552

391553
<!-- YAML
@@ -1883,6 +2045,10 @@ Node.js options that are allowed are:
18832045

18842046
<!-- node-options-node start -->
18852047

2048+
* `--allow-child-process`
2049+
* `--allow-fs-read`
2050+
* `--allow-fs-write`
2051+
* `--allow-worker`
18862052
* `--conditions`, `-C`
18872053
* `--diagnostic-dir`
18882054
* `--disable-proto`
@@ -1896,6 +2062,7 @@ Node.js options that are allowed are:
18962062
* `--experimental-loader`
18972063
* `--experimental-modules`
18982064
* `--experimental-network-imports`
2065+
* `--experimental-permission`
18992066
* `--experimental-policy`
19002067
* `--experimental-shadow-realm`
19012068
* `--experimental-specifier-resolution`
@@ -2331,9 +2498,11 @@ done
23312498
[ECMAScript module]: esm.md#modules-ecmascript-modules
23322499
[ECMAScript module loader]: esm.md#loaders
23332500
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
2501+
[File System Permissions]: permissions.md#file-system-permissions
23342502
[Modules loaders]: packages.md#modules-loaders
23352503
[Node.js issue tracker]: https://github.com/nodejs/node/issues
23362504
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
2505+
[Permission Model]: permissions.md#permission-model
23372506
[REPL]: repl.md
23382507
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
23392508
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm

doc/api/errors.md

+8
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,13 @@ APIs _not_ using `AbortSignal`s typically do not raise an error with this code.
679679
This code does not use the regular `ERR_*` convention Node.js errors use in
680680
order to be compatible with the web platform's `AbortError`.
681681

682+
<a id="ERR_ACCESS_DENIED"></a>
683+
684+
### `ERR_ACCESS_DENIED`
685+
686+
A special type of error that is triggered whenever Node.js tries to get access
687+
to a resource restricted by the [Permission Model][].
688+
682689
<a id="ERR_AMBIGUOUS_ARGUMENT"></a>
683690

684691
### `ERR_AMBIGUOUS_ARGUMENT`
@@ -3542,6 +3549,7 @@ The native call from `process.cpuUsage` could not be processed.
35423549
[JSON Web Key Elliptic Curve Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve
35433550
[JSON Web Key Types Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types
35443551
[Node.js error codes]: #nodejs-error-codes
3552+
[Permission Model]: permissions.md#permission-model
35453553
[RFC 7230 Section 3]: https://tools.ietf.org/html/rfc7230#section-3
35463554
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute
35473555
[V8's stack trace API]: https://v8.dev/docs/stack-trace-api

0 commit comments

Comments
 (0)