summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafael Gonzaga <rafael.nunu@hotmail.com>2023-02-23 15:11:51 -0300
committerGitHub <noreply@github.com>2023-02-23 18:11:51 +0000
commit00c222593e49d817281bc88a322f41f8dca95885 (patch)
tree29e6fd71f93dd98d8bb3fcd535d49ed80bdeacd5
parent42be7f6a0335c396810be91c3f3724007029f83d (diff)
downloadnode-new-00c222593e49d817281bc88a322f41f8dca95885.tar.gz
src,process: add permission model
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: https://github.com/nodejs/node/pull/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>
-rw-r--r--benchmark/fs/readfile-permission-enabled.js72
-rw-r--r--benchmark/permission/permission-fs-deny.js19
-rw-r--r--benchmark/permission/permission-fs-is-granted.js50
-rw-r--r--doc/api/cli.md169
-rw-r--r--doc/api/errors.md8
-rw-r--r--doc/api/permissions.md153
-rw-r--r--doc/api/process.md75
-rw-r--r--doc/node.115
-rw-r--r--lib/fs.js6
-rw-r--r--lib/internal/fs/utils.js17
-rw-r--r--lib/internal/modules/cjs/loader.js17
-rw-r--r--lib/internal/process/permission.js56
-rw-r--r--lib/internal/process/pre_execution.js48
-rw-r--r--lib/internal/repl/history.js7
-rw-r--r--node.gyp9
-rw-r--r--src/env-inl.h4
-rw-r--r--src/env.cc25
-rw-r--r--src/env.h3
-rw-r--r--src/env_properties.h2
-rw-r--r--src/fs_event_wrap.cc3
-rw-r--r--src/node_binding.cc1
-rw-r--r--src/node_dir.cc5
-rw-r--r--src/node_errors.h2
-rw-r--r--src/node_external_reference.h1
-rw-r--r--src/node_file.cc84
-rw-r--r--src/node_options.cc21
-rw-r--r--src/node_options.h5
-rw-r--r--src/node_worker.cc7
-rw-r--r--src/permission/child_process_permission.cc27
-rw-r--r--src/permission/child_process_permission.h30
-rw-r--r--src/permission/fs_permission.cc216
-rw-r--r--src/permission/fs_permission.h163
-rw-r--r--src/permission/permission.cc200
-rw-r--r--src/permission/permission.h73
-rw-r--r--src/permission/permission_base.h51
-rw-r--r--src/permission/worker_permission.cc26
-rw-r--r--src/permission/worker_permission.h30
-rw-r--r--src/process_wrap.cc3
-rw-r--r--src/util.h8
-rw-r--r--test/addons/no-addons/permission.js43
-rw-r--r--test/common/README.md7
-rw-r--r--test/common/tmpdir.js28
-rw-r--r--test/fixtures/permission/deny/protected-file.md3
-rw-r--r--test/fixtures/permission/deny/protected-folder/protected-file.md3
-rw-r--r--test/fixtures/permission/deny/regular-file.md0
-rw-r--r--test/parallel/test-bootstrap-modules.js2
-rw-r--r--test/parallel/test-cli-bad-options.js11
-rw-r--r--test/parallel/test-cli-permission-deny-fs.js128
-rw-r--r--test/parallel/test-permission-deny-allow-child-process-cli.js26
-rw-r--r--test/parallel/test-permission-deny-allow-worker-cli.js22
-rw-r--r--test/parallel/test-permission-deny-child-process-cli.js45
-rw-r--r--test/parallel/test-permission-deny-child-process.js52
-rw-r--r--test/parallel/test-permission-deny-fs-read.js328
-rw-r--r--test/parallel/test-permission-deny-fs-symlink-target-write.js71
-rw-r--r--test/parallel/test-permission-deny-fs-symlink.js104
-rw-r--r--test/parallel/test-permission-deny-fs-wildcard.js128
-rw-r--r--test/parallel/test-permission-deny-fs-write.js240
-rw-r--r--test/parallel/test-permission-deny-worker-threads-cli.js26
-rw-r--r--test/parallel/test-permission-deny-worker-threads.js32
-rw-r--r--test/parallel/test-permission-deny.js97
-rw-r--r--test/parallel/test-permission-experimental.js13
-rw-r--r--test/parallel/test-permission-fs-relative-path.js48
-rw-r--r--test/parallel/test-permission-fs-windows-path.js66
-rw-r--r--test/parallel/test-permission-warning-flags.js23
-rw-r--r--tools/run-worker.js8
65 files changed, 3246 insertions, 19 deletions
diff --git a/benchmark/fs/readfile-permission-enabled.js b/benchmark/fs/readfile-permission-enabled.js
new file mode 100644
index 0000000000..3053d5aa08
--- /dev/null
+++ b/benchmark/fs/readfile-permission-enabled.js
@@ -0,0 +1,72 @@
+// Call fs.readFile with permission system enabled
+// over and over again really fast.
+// Then see how many times it got called.
+'use strict';
+
+const path = require('path');
+const common = require('../common.js');
+const fs = require('fs');
+const assert = require('assert');
+
+const tmpdir = require('../../test/common/tmpdir');
+tmpdir.refresh();
+const filename = path.resolve(tmpdir.path,
+ `.removeme-benchmark-garbage-${process.pid}`);
+
+const bench = common.createBenchmark(main, {
+ duration: [5],
+ encoding: ['', 'utf-8'],
+ len: [1024, 16 * 1024 * 1024],
+ concurrent: [1, 10],
+}, {
+ flags: ['--experimental-permission', '--allow-fs-read=*', '--allow-fs-write=*'],
+});
+
+function main({ len, duration, concurrent, encoding }) {
+ try {
+ fs.unlinkSync(filename);
+ } catch {
+ // Continue regardless of error.
+ }
+ let data = Buffer.alloc(len, 'x');
+ fs.writeFileSync(filename, data);
+ data = null;
+
+ let reads = 0;
+ let benchEnded = false;
+ bench.start();
+ setTimeout(() => {
+ benchEnded = true;
+ bench.end(reads);
+ try {
+ fs.unlinkSync(filename);
+ } catch {
+ // Continue regardless of error.
+ }
+ process.exit(0);
+ }, duration * 1000);
+
+ function read() {
+ fs.readFile(filename, encoding, afterRead);
+ }
+
+ function afterRead(er, data) {
+ if (er) {
+ if (er.code === 'ENOENT') {
+ // Only OK if unlinked by the timer from main.
+ assert.ok(benchEnded);
+ return;
+ }
+ throw er;
+ }
+
+ if (data.length !== len)
+ throw new Error('wrong number of bytes returned');
+
+ reads++;
+ if (!benchEnded)
+ read();
+ }
+
+ while (concurrent--) read();
+}
diff --git a/benchmark/permission/permission-fs-deny.js b/benchmark/permission/permission-fs-deny.js
new file mode 100644
index 0000000000..29bbeb27dc
--- /dev/null
+++ b/benchmark/permission/permission-fs-deny.js
@@ -0,0 +1,19 @@
+'use strict';
+const common = require('../common.js');
+
+const configs = {
+ n: [1e5],
+ concurrent: [1, 10],
+};
+
+const options = { flags: ['--experimental-permission'] };
+
+const bench = common.createBenchmark(main, configs, options);
+
+async function main(conf) {
+ bench.start();
+ for (let i = 0; i < conf.n; i++) {
+ process.permission.deny('fs.read', ['/home/example-file-' + i]);
+ }
+ bench.end(conf.n);
+}
diff --git a/benchmark/permission/permission-fs-is-granted.js b/benchmark/permission/permission-fs-is-granted.js
new file mode 100644
index 0000000000..062ba19445
--- /dev/null
+++ b/benchmark/permission/permission-fs-is-granted.js
@@ -0,0 +1,50 @@
+'use strict';
+const common = require('../common.js');
+const fs = require('fs/promises');
+const path = require('path');
+
+const configs = {
+ n: [1e5],
+ concurrent: [1, 10],
+};
+
+const rootPath = path.resolve(__dirname, '../../..');
+
+const options = {
+ flags: [
+ '--experimental-permission',
+ `--allow-fs-read=${rootPath}`,
+ ],
+};
+
+const bench = common.createBenchmark(main, configs, options);
+
+const recursivelyDenyFiles = async (dir) => {
+ const files = await fs.readdir(dir, { withFileTypes: true });
+ for (const file of files) {
+ if (file.isDirectory()) {
+ await recursivelyDenyFiles(path.join(dir, file.name));
+ } else if (file.isFile()) {
+ process.permission.deny('fs.read', [path.join(dir, file.name)]);
+ }
+ }
+};
+
+async function main(conf) {
+ const benchmarkDir = path.join(__dirname, '../..');
+ // Get all the benchmark files and deny access to it
+ await recursivelyDenyFiles(benchmarkDir);
+
+ bench.start();
+
+ for (let i = 0; i < conf.n; i++) {
+ // Valid file in a sequence of denied files
+ process.permission.has('fs.read', benchmarkDir + '/valid-file');
+ // Denied file
+ process.permission.has('fs.read', __filename);
+ // Valid file a granted directory
+ process.permission.has('fs.read', '/tmp/example');
+ }
+
+ bench.end(conf.n);
+}
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 2c5a196986..5ca34109a8 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -100,6 +100,154 @@ If this flag is passed, the behavior can still be set to not abort through
[`process.setUncaughtExceptionCaptureCallback()`][] (and through usage of the
`node:domain` module that uses it).
+### `--allow-child-process`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+When using the [Permission Model][], the process will not be able to spawn any
+child process by default.
+Attempts to do so will throw an `ERR_ACCESS_DENIED` unless the
+user explicitly passes the `--allow-child-process` flag when starting Node.js.
+
+Example:
+
+```js
+const childProcess = require('node:child_process');
+// Attempt to bypass the permission
+childProcess.spawn('node', ['-e', 'require("fs").writeFileSync("/new-file", "example")']);
+```
+
+```console
+$ node --experimental-permission --allow-fs-read=* index.js
+node:internal/child_process:388
+ const err = this._handle.spawn(options);
+ ^
+Error: Access to this API has been restricted
+ at ChildProcess.spawn (node:internal/child_process:388:28)
+ at Object.spawn (node:child_process:723:9)
+ at Object.<anonymous> (/home/index.js:3:14)
+ at Module._compile (node:internal/modules/cjs/loader:1120:14)
+ at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
+ at Module.load (node:internal/modules/cjs/loader:998:32)
+ at Module._load (node:internal/modules/cjs/loader:839:12)
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
+ at node:internal/main/run_main_module:17:47 {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess'
+}
+```
+
+### `--allow-fs-read`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+This flag configures file system read permissions using
+the [Permission Model][].
+
+The valid arguments for the `--allow-fs-read` flag are:
+
+* `*` - To allow the `FileSystemRead` operations.
+* Paths delimited by comma (,) to manage `FileSystemRead` (reading) operations.
+
+Examples can be found in the [File System Permissions][] documentation.
+
+Relative paths are NOT yet supported by the CLI flag.
+
+The initializer module also needs to be allowed. Consider the following example:
+
+```console
+$ node --experimental-permission t.js
+node:internal/modules/cjs/loader:162
+ const result = internalModuleStat(filename);
+ ^
+
+Error: Access to this API has been restricted
+ at stat (node:internal/modules/cjs/loader:162:18)
+ at Module._findPath (node:internal/modules/cjs/loader:640:16)
+ at resolveMainPath (node:internal/modules/run_main:15:25)
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:53:24)
+ at node:internal/main/run_main_module:23:47 {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: '/Users/rafaelgss/repos/os/node/t.js'
+}
+```
+
+The process needs to have access to the `index.js` module:
+
+```console
+$ node --experimental-permission --allow-fs-read=/path/to/index.js index.js
+```
+
+### `--allow-fs-write`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+This flag configures file system write permissions using
+the [Permission Model][].
+
+The valid arguments for the `--allow-fs-write` flag are:
+
+* `*` - To allow the `FileSystemWrite` operations.
+* Paths delimited by comma (,) to manage `FileSystemWrite` (writing) operations.
+
+Examples can be found in the [File System Permissions][] documentation.
+
+Relative paths are NOT supported through the CLI flag.
+
+### `--allow-worker`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+When using the [Permission Model][], the process will not be able to create any
+worker threads by default.
+For security reasons, the call will throw an `ERR_ACCESS_DENIED` unless the
+user explicitly pass the flag `--allow-worker` in the main Node.js process.
+
+Example:
+
+```js
+const { Worker } = require('node:worker_threads');
+// Attempt to bypass the permission
+new Worker(__filename);
+```
+
+```console
+$ node --experimental-permission --allow-fs-read=* index.js
+node:internal/worker:188
+ this[kHandle] = new WorkerImpl(url,
+ ^
+
+Error: Access to this API has been restricted
+ at new Worker (node:internal/worker:188:21)
+ at Object.<anonymous> (/home/index.js.js:3:1)
+ at Module._compile (node:internal/modules/cjs/loader:1120:14)
+ at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
+ at Module.load (node:internal/modules/cjs/loader:998:32)
+ at Module._load (node:internal/modules/cjs/loader:839:12)
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
+ at node:internal/main/run_main_module:17:47 {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'WorkerThreads'
+}
+```
+
### `--build-snapshot`
<!-- YAML
@@ -386,6 +534,20 @@ added:
Enable experimental support for the `https:` protocol in `import` specifiers.
+### `--experimental-permission`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+Enable the Permission Model for current process. When enabled, the
+following permissions are restricted:
+
+* File System - manageable through
+ \[`--allow-fs-read`]\[],\[`allow-fs-write`]\[] flags
+* Child Process - manageable through \[`--allow-child-process`]\[] flag
+* Worker Threads - manageable through \[`--allow-worker`]\[] flag
+
### `--experimental-policy`
<!-- YAML
@@ -1883,6 +2045,10 @@ Node.js options that are allowed are:
<!-- node-options-node start -->
+* `--allow-child-process`
+* `--allow-fs-read`
+* `--allow-fs-write`
+* `--allow-worker`
* `--conditions`, `-C`
* `--diagnostic-dir`
* `--disable-proto`
@@ -1896,6 +2062,7 @@ Node.js options that are allowed are:
* `--experimental-loader`
* `--experimental-modules`
* `--experimental-network-imports`
+* `--experimental-permission`
* `--experimental-policy`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
@@ -2331,9 +2498,11 @@ done
[ECMAScript module]: esm.md#modules-ecmascript-modules
[ECMAScript module loader]: esm.md#loaders
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+[File System Permissions]: permissions.md#file-system-permissions
[Modules loaders]: packages.md#modules-loaders
[Node.js issue tracker]: https://github.com/nodejs/node/issues
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
+[Permission Model]: permissions.md#permission-model
[REPL]: repl.md
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm
diff --git a/doc/api/errors.md b/doc/api/errors.md
index a895dd5a5a..9139194719 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -679,6 +679,13 @@ APIs _not_ using `AbortSignal`s typically do not raise an error with this code.
This code does not use the regular `ERR_*` convention Node.js errors use in
order to be compatible with the web platform's `AbortError`.
+<a id="ERR_ACCESS_DENIED"></a>
+
+### `ERR_ACCESS_DENIED`
+
+A special type of error that is triggered whenever Node.js tries to get access
+to a resource restricted by the [Permission Model][].
+
<a id="ERR_AMBIGUOUS_ARGUMENT"></a>
### `ERR_AMBIGUOUS_ARGUMENT`
@@ -3542,6 +3549,7 @@ The native call from `process.cpuUsage` could not be processed.
[JSON Web Key Elliptic Curve Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve
[JSON Web Key Types Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types
[Node.js error codes]: #nodejs-error-codes
+[Permission Model]: permissions.md#permission-model
[RFC 7230 Section 3]: https://tools.ietf.org/html/rfc7230#section-3
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute
[V8's stack trace API]: https://v8.dev/docs/stack-trace-api
diff --git a/doc/api/permissions.md b/doc/api/permissions.md
index a4be1a7c39..b31d2934b3 100644
--- a/doc/api/permissions.md
+++ b/doc/api/permissions.md
@@ -10,6 +10,12 @@ be accessed by other modules.
This can be used to control what modules can be accessed by third-party
dependencies, for example.
+* [Process-based permissions](#process-based-permissions) control the Node.js
+ process's access to resources.
+ The resource can be entirely allowed or denied, or actions related to it can
+ be controlled. For example, file system reads can be allowed while denying
+ writes.
+
If you find a potential security vulnerability, please refer to our
[Security Policy][].
@@ -440,7 +446,154 @@ not adopt the origin of the `blob:` URL.
Additionally, import maps only work on `import` so it may be desirable to add a
`"import"` condition to all dependency mappings.
+## Process-based permissions
+
+### Permission Model
+
+<!-- type=misc -->
+
+> Stability: 1 - Experimental
+
+<!-- name=permission-model -->
+
+The Node.js Permission Model is a mechanism for restricting access to specific
+resources during execution.
+The API exists behind a flag [`--experimental-permission`][] which when enabled,
+will restrict access to all available permissions.
+
+The available permissions are documented by the [`--experimental-permission`][]
+flag.
+
+When starting Node.js with `--experimental-permission`,
+the ability to access the file system, spawn processes, and
+use `node:worker_threads` will be restricted.
+
+```console
+$ node --experimental-permission index.js
+node:internal/modules/cjs/loader:171
+ const result = internalModuleStat(filename);
+ ^
+
+Error: Access to this API has been restricted
+ at stat (node:internal/modules/cjs/loader:171:18)
+ at Module._findPath (node:internal/modules/cjs/loader:627:16)
+ at resolveMainPath (node:internal/modules/run_main:19:25)
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
+ at node:internal/main/run_main_module:23:47 {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead'
+}
+```
+
+Allowing access to spawning a process and creating worker threads can be done
+using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.
+
+#### Runtime API
+
+When enabling the Permission Model through the [`--experimental-permission`][]
+flag a new property `permission` is added to the `process` object.
+This property contains two functions:
+
+##### `permission.deny(scope [,parameters])`
+
+API call to deny permissions at runtime ([`permission.deny()`][])
+
+```js
+process.permission.deny('fs'); // Deny permissions to ALL fs operations
+
+// Deny permissions to ALL FileSystemWrite operations
+process.permission.deny('fs.write');
+// deny FileSystemWrite permissions to the protected-folder
+process.permission.deny('fs.write', ['/home/rafaelgss/protected-folder']);
+// Deny permissions to ALL FileSystemRead operations
+process.permission.deny('fs.read');
+// deny FileSystemRead permissions to the protected-folder
+process.permission.deny('fs.read', ['/home/rafaelgss/protected-folder']);
+```
+
+##### `permission.has(scope ,parameters)`
+
+API call to check permissions at runtime ([`permission.has()`][])
+
+```js
+process.permission.has('fs.write'); // true
+process.permission.has('fs.write', '/home/rafaelgss/protected-folder'); // true
+
+process.permission.deny('fs.write', '/home/rafaelgss/protected-folder');
+
+process.permission.has('fs.write'); // true
+process.permission.has('fs.write', '/home/rafaelgss/protected-folder'); // false
+```
+
+#### File System Permissions
+
+To allow access to the file system, use the [`--allow-fs-read`][] and
+[`--allow-fs-write`][] flags:
+
+```console
+$ node --experimental-permission --allow-fs-read=* --allow-fs-write=* index.js
+Hello world!
+(node:19836) ExperimentalWarning: Permission is an experimental feature
+(Use `node --trace-warnings ...` to show where the warning was created)
+```
+
+The valid arguments for both flags are:
+
+* `*` - To allow the all operations to given scope (read/write).
+* Paths delimited by comma (,) to manage reading/writing operations.
+
+Example:
+
+* `--allow-fs-read=*` - It will allow all `FileSystemRead` operations.
+* `--allow-fs-write=*` - It will allow all `FileSystemWrite` operations.
+* `--allow-fs-write=/tmp/` - It will allow `FileSystemWrite` access to the `/tmp/`
+ folder.
+* `--allow-fs-read=/tmp/,/home/.gitignore` - It allows `FileSystemRead` access
+ to the `/tmp/` folder **and** the `/home/.gitignore` path.
+
+Wildcards are supported too:
+
+* `--allow-fs-read:/home/test*` will allow read access to everything
+ that matches the wildcard. e.g: `/home/test/file1` or `/home/test2`
+
+There are constraints you need to know before using this system:
+
+* Native modules are restricted by default when using the Permission Model.
+* Relative paths are not supported through the CLI (`--allow-fs-*`).
+ The runtime API supports relative paths.
+* The model does not inherit to a child node process.
+* The model does not inherit to a worker thread.
+* When creating symlinks the target (first argument) should have read and
+ write access.
+* Permission changes are not retroactively applied to existing resources.
+ Consider the following snippet:
+ ```js
+ const fs = require('node:fs');
+
+ // Open a fd
+ const fd = fs.openSync('./README.md', 'r');
+ // Then, deny access to all fs.read operations
+ process.permission.deny('fs.read');
+ // This call will NOT fail and the file will be read
+ const data = fs.readFileSync(fd);
+ ```
+
+Therefore, when possible, apply the permissions rules before any statement:
+
+```js
+process.permission.deny('fs.read');
+const fd = fs.openSync('./README.md', 'r');
+// Error: Access to this API has been restricted
+```
+
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
+[`--allow-child-process`]: cli.md#--allow-child-process
+[`--allow-fs-read`]: cli.md#--allow-fs-read
+[`--allow-fs-write`]: cli.md#--allow-fs-write
+[`--allow-worker`]: cli.md#--allow-worker
+[`--experimental-permission`]: cli.md#--experimental-permission
+[`permission.deny()`]: process.md#processpermissiondenyscope-reference
+[`permission.has()`]: process.md#processpermissionhasscope-reference
[import maps]: https://url.spec.whatwg.org/#relative-url-with-fragment-string
[relative-url string]: https://url.spec.whatwg.org/#relative-url-with-fragment-string
[special schemes]: https://url.spec.whatwg.org/#special-scheme
diff --git a/doc/api/process.md b/doc/api/process.md
index 33b60f69a2..c6537aa44d 100644
--- a/doc/api/process.md
+++ b/doc/api/process.md
@@ -2618,6 +2618,79 @@ the [`'warning'` event][process_warning] and the
[`emitWarning()` method][process_emit_warning] for more information about this
flag's behavior.
+## `process.permission`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* {Object}
+
+This API is available through the [`--experimental-permission`][] flag.
+
+`process.permission` is an object whose methods are used to manage permissions
+for the current process. Additional documentation is available in the
+[Permission Model][].
+
+### `process.permission.deny(scope[, reference])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `scopes` {string}
+* `reference` {Array}
+* Returns: {boolean}
+
+Deny permissions at runtime.
+
+The available scopes are:
+
+* `fs` - All File System
+* `fs.read` - File System read operations
+* `fs.write` - File System write operations
+
+The reference has a meaning based on the provided scope. For example,
+the reference when the scope is File System means files and folders.
+
+```js
+// Deny READ operations to the ./README.md file
+process.permission.deny('fs.read', ['./README.md']);
+// Deny ALL WRITE operations
+process.permission.deny('fs.write');
+```
+
+### `process.permission.has(scope[, reference])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `scopes` {string}
+* `reference` {string}
+* Returns: {boolean}
+
+Verifies that the process is able to access the given scope and reference.
+If no reference is provided, a global scope is assumed, for instance,
+`process.permission.has('fs.read')` will check if the process has ALL
+file system read permissions.
+
+The reference has a meaning based on the provided scope. For example,
+the reference when the scope is File System means files and folders.
+
+The available scopes are:
+
+* `fs` - All File System
+* `fs.read` - File System read operations
+* `fs.write` - File System write operations
+
+```js
+// Check if the process has permission to read the README file
+process.permission.has('fs.read', './README.md');
+// Check if the process has read permission operations
+process.permission.has('fs.read');
+```
+
## `process.pid`
<!-- YAML
@@ -3868,6 +3941,7 @@ cases:
[Duplex]: stream.md#duplex-and-transform-streams
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#process-nexttick
[LTS]: https://github.com/nodejs/Release
+[Permission Model]: permissions.md#permission-model
[Readable]: stream.md#readable-streams
[Signal Events]: #signal-events
[Source Map]: https://sourcemaps.info/spec.html
@@ -3877,6 +3951,7 @@ cases:
[`'exit'`]: #event-exit
[`'message'`]: child_process.md#event-message
[`'uncaughtException'`]: #event-uncaughtexception
+[`--experimental-permission`]: cli.md#--experimental-permission
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
[`Buffer`]: buffer.md
[`ChildProcess.disconnect()`]: child_process.md#subprocessdisconnect
diff --git a/doc/node.1 b/doc/node.1
index e316304ec1..21ff43a03f 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -76,6 +76,18 @@ the next argument will be used as a script filename.
.It Fl -abort-on-uncaught-exception
Aborting instead of exiting causes a core file to be generated for analysis.
.
+.It Fl -allow-fs-read
+Allow file system read access when using the permission model.
+.
+.It Fl -allow-fs-write
+Allow file system write access when using the permission model.
+.
+.It Fl -allow-child-process
+Allow spawning process when using the permission model.
+.
+.It Fl -allow-worker
+Allow creating worker threads when using the permission model.
+.
.It Fl -completion-bash
Print source-able bash completion script for Node.js.
.
@@ -154,6 +166,9 @@ to use as a custom module loader.
.It Fl -experimental-network-imports
Enable experimental support for loading modules using `import` over `https:`.
.
+.It Fl -experimental-permission
+Enable the experimental permission model.
+.
.It Fl -experimental-policy
Use the specified file as a security policy.
.
diff --git a/lib/fs.js b/lib/fs.js
index be462fd820..25534c838d 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -104,6 +104,7 @@ const {
getValidMode,
handleErrorFromBinding,
nullCheck,
+ possiblyTransformPath,
preprocessSymlinkDestination,
Stats,
getStatFsFromBinding,
@@ -2338,16 +2339,17 @@ function watch(filename, options, listener) {
let watcher;
const watchers = require('internal/fs/watchers');
+ const path = possiblyTransformPath(filename);
// TODO(anonrig): Remove non-native watcher when/if libuv supports recursive.
// As of November 2022, libuv does not support recursive file watch on all platforms,
// e.g. Linux due to the limitations of inotify.
if (options.recursive && !isOSX && !isWindows) {
const nonNativeWatcher = require('internal/fs/recursive_watch');
watcher = new nonNativeWatcher.FSWatcher(options);
- watcher[watchers.kFSWatchStart](filename);
+ watcher[watchers.kFSWatchStart](path);
} else {
watcher = new watchers.FSWatcher();
- watcher[watchers.kFSWatchStart](filename,
+ watcher[watchers.kFSWatchStart](path,
options.persistent,
options.recursive,
options.encoding);
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 313d9ab136..b32527f9cd 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -23,6 +23,8 @@ const {
TypedArrayPrototypeIncludes,
} = primordials;
+const permission = require('internal/process/permission');
+
const { Buffer } = require('buffer');
const {
codes: {
@@ -699,10 +701,22 @@ const validatePath = hideStackFrames((path, propName = 'path') => {
}
});
+// TODO(rafaelgss): implement the path.resolve on C++ side
+// See: https://github.com/nodejs/node/pull/44004#discussion_r930958420
+// The permission model needs the absolute path for the fs_permission
+function possiblyTransformPath(path) {
+ if (permission.isEnabled()) {
+ if (typeof path === 'string' && !pathModule.isAbsolute(path)) {
+ return pathModule.resolve(path);
+ }
+ }
+ return path;
+}
+
const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => {
const path = toPathIfFileURL(fileURLOrPath);
validatePath(path, propName);
- return path;
+ return possiblyTransformPath(path);
});
const getValidatedFd = hideStackFrames((fd, propName = 'fd') => {
@@ -928,6 +942,7 @@ module.exports = {
getValidMode,
handleErrorFromBinding,
nullCheck,
+ possiblyTransformPath,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
getStatFsFromBinding,
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 8d602df88a..c0b8cbc8a1 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -121,6 +121,8 @@ const getCascadedLoader = getLazy(
() => require('internal/process/esm_loader').esmLoader,
);
+const permission = require('internal/process/permission');
+
// Whether any user-provided CJS modules had been loaded (executed).
// Used for internal assertions.
let hasLoadedAnyUserCJSModule = false;
@@ -415,9 +417,15 @@ ObjectDefineProperty(Module, '_readPackage', {
function readPackageScope(checkPath) {
const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep);
let separatorIndex;
+ const enabledPermission = permission.isEnabled();
do {
separatorIndex = StringPrototypeLastIndexOf(checkPath, sep);
checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex);
+ // Stop the search when the process doesn't have permissions
+ // to walk upwards
+ if (enabledPermission && !permission.has('fs.read', checkPath)) {
+ return false;
+ }
if (StringPrototypeEndsWith(checkPath, sep + 'node_modules'))
return false;
const pjson = _readPackage(checkPath + sep);
@@ -639,9 +647,14 @@ Module._findPath = function(request, paths, isMain) {
// For each path
for (let i = 0; i < paths.length; i++) {
- // Don't search further if path doesn't exist and request is inside the path
+ // Don't search further if path doesn't exist
+ // or doesn't have permission to it
const curPath = paths[i];
- if (insidePath && curPath && _stat(curPath) < 1) continue;
+ if (insidePath && curPath &&
+ ((permission.isEnabled() && !permission.has('fs.read', curPath)) || _stat(curPath) < 1)
+ ) {
+ continue;
+ }
if (!absoluteRequest) {
const exportsResolved = resolveExports(curPath, request);
diff --git a/lib/internal/process/permission.js b/lib/internal/process/permission.js
new file mode 100644
index 0000000000..298cb140e5
--- /dev/null
+++ b/lib/internal/process/permission.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const {
+ ObjectFreeze,
+ ArrayPrototypePush,
+} = primordials;
+
+const permission = internalBinding('permission');
+const { validateString, validateArray } = require('internal/validators');
+const { isAbsolute, resolve } = require('path');
+
+let experimentalPermission;
+
+module.exports = ObjectFreeze({
+ __proto__: null,
+ isEnabled() {
+ if (experimentalPermission === undefined) {
+ const { getOptionValue } = require('internal/options');
+ experimentalPermission = getOptionValue('--experimental-permission');
+ }
+ return experimentalPermission;
+ },
+ deny(scope, references) {
+ validateString(scope, 'scope');
+ if (references == null) {
+ return permission.deny(scope, references);
+ }
+
+ validateArray(references, 'references');
+ // TODO(rafaelgss): change to call fs_permission.resolve when available
+ const normalizedParams = [];
+ for (let i = 0; i < references.length; ++i) {
+ if (isAbsolute(references[i])) {
+ ArrayPrototypePush(normalizedParams, references[i]);
+ } else {
+ // TODO(aduh95): add support for WHATWG URLs and Uint8Arrays.
+ ArrayPrototypePush(normalizedParams, resolve(references[i]));
+ }
+ }
+
+ return permission.deny(scope, normalizedParams);
+ },
+
+ has(scope, reference) {
+ validateString(scope, 'scope');
+ if (reference != null) {
+ // TODO: add support for WHATWG URLs and Uint8Arrays.
+ validateString(reference, 'reference');
+ if (!isAbsolute(reference)) {
+ return permission.has(scope, resolve(reference));
+ }
+ }
+
+ return permission.has(scope, reference);
+ },
+});
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index ebc699ef1d..7dcbe07e65 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -1,6 +1,7 @@
'use strict';
const {
+ ArrayPrototypeForEach,
NumberParseInt,
ObjectDefineProperties,
ObjectDefineProperty,
@@ -27,6 +28,7 @@ const {
ERR_INVALID_THIS,
ERR_MANIFEST_ASSERT_INTEGRITY,
ERR_NO_CRYPTO,
+ ERR_MISSING_OPTION,
} = require('internal/errors').codes;
const assert = require('internal/assert');
const {
@@ -71,6 +73,10 @@ function prepareExecution(options) {
setupDebugEnv();
// Process initial diagnostic reporting configuration, if present.
initializeReport();
+
+ // Load permission system API
+ initializePermission();
+
initializeSourceMapsHandlers();
initializeDeprecations();
initializeWASI();
@@ -498,6 +504,48 @@ function initializeClusterIPC() {
}
}
+function initializePermission() {
+ const experimentalPermission = getOptionValue('--experimental-permission');
+ if (experimentalPermission) {
+ process.emitWarning('Permission is an experimental feature',
+ 'ExperimentalWarning');
+ const { has, deny } = require('internal/process/permission');
+ const warnFlags = [
+ '--allow-child-process',
+ '--allow-worker',
+ ];
+ for (const flag of warnFlags) {
+ if (getOptionValue(flag)) {
+ process.emitWarning(
+ `The flag ${flag} must be used with extreme caution. ` +
+ 'It could invalidate the permission model.', 'SecurityWarning');
+ }
+ }
+
+ ObjectDefineProperty(process, 'permission', {
+ __proto__: null,
+ enumerable: true,
+ configurable: false,
+ value: {
+ has,
+ deny,
+ },
+ });
+ } else {
+ const availablePermissionFlags = [
+ '--allow-fs-read',
+ '--allow-fs-write',
+ '--allow-child-process',
+ '--allow-worker',
+ ];
+ ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
+ if (getOptionValue(flag)) {
+ throw new ERR_MISSING_OPTION('--experimental-permission');
+ }
+ });
+ }
+}
+
function readPolicyFromDisk() {
const experimentalPolicy = getOptionValue('--experimental-policy');
if (experimentalPolicy) {
diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js
index d580c258a4..49efcd1ef1 100644
--- a/lib/internal/repl/history.js
+++ b/lib/internal/repl/history.js
@@ -15,6 +15,7 @@ const os = require('os');
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
debug = fn;
});
+const permission = require('internal/process/permission');
const { clearTimeout, setTimeout } = require('timers');
const noop = FunctionPrototype;
@@ -53,6 +54,12 @@ function setupHistory(repl, historyPath, ready) {
}
}
+ if (permission.isEnabled() && permission.has('fs.write', historyPath) === false) {
+ _writeToOutput(repl, '\nAccess to FileSystemOut is restricted.\n' +
+ 'REPL session history will not be persisted.\n');
+ return ready(null, repl);
+ }
+
let timer = null;
let writing = false;
let pending = false;
diff --git a/node.gyp b/node.gyp
index 04ebb0ae56..cb46307ef6 100644
--- a/node.gyp
+++ b/node.gyp
@@ -544,6 +544,10 @@
'src/node_watchdog.cc',
'src/node_worker.cc',
'src/node_zlib.cc',
+ 'src/permission/child_process_permission.cc',
+ 'src/permission/fs_permission.cc',
+ 'src/permission/permission.cc',
+ 'src/permission/worker_permission.cc',
'src/pipe_wrap.cc',
'src/process_wrap.cc',
'src/signal_wrap.cc',
@@ -655,6 +659,11 @@
'src/node_wasi.h',
'src/node_watchdog.h',
'src/node_worker.h',
+ 'src/permission/child_process_permission.h',
+ 'src/permission/fs_permission.h',
+ 'src/permission/permission.h',
+ 'src/permission/permission_node.h',
+ 'src/permission/worker_permission.h',
'src/pipe_wrap.h',
'src/req_wrap.h',
'src/req_wrap-inl.h',
diff --git a/src/env-inl.h b/src/env-inl.h
index d84d28c706..6b4d63616a 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -293,6 +293,10 @@ inline TickInfo* Environment::tick_info() {
return &tick_info_;
}
+inline permission::Permission* Environment::permission() {
+ return &permission_;
+}
+
inline uint64_t Environment::timer_base() const {
return timer_base_;
}
diff --git a/src/env.cc b/src/env.cc
index e315e41ced..c730401c7a 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -756,6 +756,31 @@ Environment::Environment(IsolateData* isolate_data,
"args",
std::move(traced_value));
}
+
+ if (options_->experimental_permission) {
+ permission()->EnablePermissions();
+ // If any permission is set the process shouldn't be able to neither
+ // spawn/worker nor use addons unless explicitly allowed by the user
+ if (!options_->allow_fs_read.empty() || !options_->allow_fs_write.empty()) {
+ options_->allow_native_addons = false;
+ if (!options_->allow_child_process) {
+ permission()->Deny(permission::PermissionScope::kChildProcess, {});
+ }
+ if (!options_->allow_worker_threads) {
+ permission()->Deny(permission::PermissionScope::kWorkerThreads, {});
+ }
+ }
+
+ if (!options_->allow_fs_read.empty()) {
+ permission()->Apply(options_->allow_fs_read,
+ permission::PermissionScope::kFileSystemRead);
+ }
+
+ if (!options_->allow_fs_write.empty()) {
+ permission()->Apply(options_->allow_fs_write,
+ permission::PermissionScope::kFileSystemWrite);
+ }
+ }
}
void Environment::InitializeMainContext(Local<Context> context,
diff --git a/src/env.h b/src/env.h
index 677c03d952..629b99a920 100644
--- a/src/env.h
+++ b/src/env.h
@@ -43,6 +43,7 @@
#include "node_perf_common.h"
#include "node_realm.h"
#include "node_snapshotable.h"
+#include "permission/permission.h"
#include "req_wrap.h"
#include "util.h"
#include "uv.h"
@@ -660,6 +661,7 @@ class Environment : public MemoryRetainer {
inline AliasedInt32Array& timeout_info();
inline TickInfo* tick_info();
inline uint64_t timer_base() const;
+ inline permission::Permission* permission();
inline std::shared_ptr<KVStore> env_vars();
inline void set_env_vars(std::shared_ptr<KVStore> env_vars);
@@ -996,6 +998,7 @@ class Environment : public MemoryRetainer {
ImmediateInfo immediate_info_;
AliasedInt32Array timeout_info_;
TickInfo tick_info_;
+ permission::Permission permission_;
const uint64_t timer_base_;
std::shared_ptr<KVStore> env_vars_;
bool printed_error_ = false;
diff --git a/src/env_properties.h b/src/env_properties.h
index fcae9d8887..9abcb8d9d7 100644
--- a/src/env_properties.h
+++ b/src/env_properties.h
@@ -232,6 +232,7 @@
V(password_string, "password") \
V(path_string, "path") \
V(pending_handle_string, "pendingHandle") \
+ V(permission_string, "permission") \
V(pid_string, "pid") \
V(ping_rtt_string, "pingRTT") \
V(pipe_source_string, "pipeSource") \
@@ -259,6 +260,7 @@
V(rename_string, "rename") \
V(replacement_string, "replacement") \
V(require_string, "require") \
+ V(resource_string, "resource") \
V(retry_string, "retry") \
V(salt_length_string, "saltLength") \
V(scheme_string, "scheme") \
diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc
index 048b72666c..400a84af1f 100644
--- a/src/fs_event_wrap.cc
+++ b/src/fs_event_wrap.cc
@@ -24,6 +24,7 @@
#include "handle_wrap.h"
#include "node.h"
#include "node_external_reference.h"
+#include "permission/permission.h"
#include "string_bytes.h"
namespace node {
@@ -146,6 +147,8 @@ void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, *path);
unsigned int flags = 0;
if (args[2]->IsTrue())
diff --git a/src/node_binding.cc b/src/node_binding.cc
index db607ea298..3c97c73964 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -58,6 +58,7 @@
V(options) \
V(os) \
V(performance) \
+ V(permission) \
V(pipe_wrap) \
V(process_wrap) \
V(process_methods) \
diff --git a/src/node_dir.cc b/src/node_dir.cc
index 0ffee1cb8f..0bef2b8927 100644
--- a/src/node_dir.cc
+++ b/src/node_dir.cc
@@ -1,8 +1,9 @@
#include "node_dir.h"
+#include "memory_tracker-inl.h"
#include "node_external_reference.h"
#include "node_file-inl.h"
#include "node_process-inl.h"
-#include "memory_tracker-inl.h"
+#include "permission/permission.h"
#include "util.h"
#include "tracing/trace_event.h"
@@ -366,6 +367,8 @@ static void OpenDir(const FunctionCallbackInfo<Value>& args) {
BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);
diff --git a/src/node_errors.h b/src/node_errors.h
index 1c0c342df3..2bfb6128d9 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -30,6 +30,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
// a `Local<Value>` containing the TypeError with proper code and message
#define ERRORS_WITH_CODE(V) \
+ V(ERR_ACCESS_DENIED, Error) \
V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \
V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \
V(ERR_BUFFER_TOO_LARGE, Error) \
@@ -124,6 +125,7 @@ ERRORS_WITH_CODE(V)
// Errors with predefined static messages
#define PREDEFINED_ERROR_MESSAGES(V) \
+ V(ERR_ACCESS_DENIED, "Access to this API has been restricted") \
V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, \
"Buffer is not available for the current Context") \
V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \
diff --git a/src/node_external_reference.h b/src/node_external_reference.h
index 38ba3b21a7..789770c956 100644
--- a/src/node_external_reference.h
+++ b/src/node_external_reference.h
@@ -78,6 +78,7 @@ class ExternalReferenceRegistry {
V(options) \
V(os) \
V(performance) \
+ V(permission) \
V(process_methods) \
V(process_object) \
V(report) \
diff --git a/src/node_file.cc b/src/node_file.cc
index 388d9bf32c..c6d22b51f2 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -19,13 +19,14 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "node_file.h" // NOLINT(build/include_inline)
-#include "node_file-inl.h"
#include "aliased_buffer.h"
#include "memory_tracker-inl.h"
#include "node_buffer.h"
#include "node_external_reference.h"
+#include "node_file-inl.h"
#include "node_process-inl.h"
#include "node_stat_watcher.h"
+#include "permission/permission.h"
#include "util-inl.h"
#include "tracing/trace_event.h"
@@ -961,6 +962,7 @@ void AfterScanDir(uv_fs_t* req) {
void Access(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
+
Isolate* isolate = env->isolate();
HandleScope scope(isolate);
@@ -972,6 +974,8 @@ void Access(const FunctionCallbackInfo<Value>& args) {
BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
FSReqBase* req_wrap_async = GetReqWrap(args, 2);
if (req_wrap_async != nullptr) { // access(path, mode, req)
@@ -1022,6 +1026,8 @@ static void InternalModuleReadJSON(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
node::Utf8Value path(isolate, args[0]);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
if (strlen(*path) != path.length()) {
args.GetReturnValue().Set(Array::New(isolate));
@@ -1118,6 +1124,8 @@ static void InternalModuleStat(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
node::Utf8Value path(env->isolate(), args[0]);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
uv_fs_t req;
int rc = uv_fs_stat(env->event_loop(), &req, *path, nullptr);
@@ -1139,6 +1147,8 @@ static void Stat(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
bool use_bigint = args[1]->IsTrue();
FSReqBase* req_wrap_async = GetReqWrap(args, 2, use_bigint);
@@ -1280,8 +1290,17 @@ static void Symlink(const FunctionCallbackInfo<Value>& args) {
BufferValue target(isolate, args[0]);
CHECK_NOT_NULL(*target);
+ auto target_view = target.ToStringView();
+ // To avoid bypass the symlink target should be allowed to read and write
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, target_view);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, target_view);
+
BufferValue path(isolate, args[1]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
CHECK(args[2]->IsInt32());
int flags = args[2].As<Int32>()->Value();
@@ -1348,6 +1367,8 @@ static void ReadLink(const FunctionCallbackInfo<Value>& args) {
BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);
@@ -1393,8 +1414,18 @@ static void Rename(const FunctionCallbackInfo<Value>& args) {
BufferValue old_path(isolate, args[0]);
CHECK_NOT_NULL(*old_path);
+ auto view_old_path = old_path.ToStringView();
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, view_old_path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, view_old_path);
+
BufferValue new_path(isolate, args[1]);
CHECK_NOT_NULL(*new_path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env,
+ permission::PermissionScope::kFileSystemWrite,
+ new_path.ToStringView());
FSReqBase* req_wrap_async = GetReqWrap(args, 2);
if (req_wrap_async != nullptr) {
@@ -1498,6 +1529,8 @@ static void Unlink(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
FSReqBase* req_wrap_async = GetReqWrap(args, 1);
if (req_wrap_async != nullptr) {
@@ -1522,6 +1555,8 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
FSReqBase* req_wrap_async = GetReqWrap(args, 1); // rmdir(path, req)
if (req_wrap_async != nullptr) {
@@ -1729,6 +1764,8 @@ static void MKDir(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
CHECK(args[1]->IsInt32());
const int mode = args[1].As<Int32>()->Value();
@@ -1827,6 +1864,8 @@ static void ReadDir(const FunctionCallbackInfo<Value>& args) {
BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);
@@ -1925,6 +1964,23 @@ static void Open(const FunctionCallbackInfo<Value>& args) {
CHECK(args[2]->IsInt32());
const int mode = args[2].As<Int32>()->Value();
+ auto pathView = path.ToStringView();
+ // Open can be called either in write or read
+ if (flags == O_RDWR) {
+ // TODO(rafaelgss): it can be optimized to avoid O(2*n)
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, pathView);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, pathView);
+ } else if ((flags & ~(UV_FS_O_RDONLY | UV_FS_O_SYNC)) == 0) {
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, pathView);
+ } else if ((flags & (UV_FS_O_APPEND | UV_FS_O_TRUNC | UV_FS_O_CREAT |
+ UV_FS_O_WRONLY)) != 0) {
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, pathView);
+ }
+
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
if (req_wrap_async != nullptr) { // open(path, flags, mode, req)
req_wrap_async->set_is_plain_open(true);
@@ -1954,6 +2010,9 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
+ auto pathView = path.ToStringView();
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, pathView);
CHECK(args[1]->IsInt32());
const int flags = args[1].As<Int32>()->Value();
@@ -1961,6 +2020,22 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
CHECK(args[2]->IsInt32());
const int mode = args[2].As<Int32>()->Value();
+ // Open can be called either in write or read
+ if (flags == O_RDWR) {
+ // TODO(rafaelgss): it can be optimized to avoid O(2*n)
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, pathView);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, pathView);
+ } else if ((flags & ~(UV_FS_O_RDONLY | UV_FS_O_SYNC)) == 0) {
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, pathView);
+ } else if ((flags & (UV_FS_O_APPEND | UV_FS_O_TRUNC | UV_FS_O_CREAT |
+ UV_FS_O_WRONLY)) != 0) {
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, pathView);
+ }
+
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
if (req_wrap_async != nullptr) { // openFileHandle(path, flags, mode, req)
FS_ASYNC_TRACE_BEGIN1(
@@ -1992,9 +2067,13 @@ static void CopyFile(const FunctionCallbackInfo<Value>& args) {
BufferValue src(isolate, args[0]);
CHECK_NOT_NULL(*src);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemRead, src.ToStringView());
BufferValue dest(isolate, args[1]);
CHECK_NOT_NULL(*dest);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, dest.ToStringView());
CHECK(args[2]->IsInt32());
const int flags = args[2].As<Int32>()->Value();
@@ -2138,7 +2217,6 @@ static void WriteString(const FunctionCallbackInfo<Value>& args) {
const int argc = args.Length();
CHECK_GE(argc, 4);
-
CHECK(args[0]->IsInt32());
const int fd = args[0].As<Int32>()->Value();
@@ -2503,6 +2581,8 @@ static void UTimes(const FunctionCallbackInfo<Value>& args) {
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
CHECK(args[1]->IsNumber());
const double atime = args[1].As<Number>()->Value();
diff --git a/src/node_options.cc b/src/node_options.cc
index e7b0ba1073..3551b38fca 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -401,6 +401,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental ES Module import.meta.resolve() support",
&EnvironmentOptions::experimental_import_meta_resolve,
kAllowedInEnvvar);
+ AddOption("--experimental-permission",
+ "enable the permission system",
+ &EnvironmentOptions::experimental_permission,
+ kAllowedInEnvvar,
+ false);
AddOption("--experimental-policy",
"use the specified file as a "
"security policy",
@@ -415,6 +420,22 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::experimental_policy_integrity,
kAllowedInEnvvar);
Implies("--policy-integrity", "[has_policy_integrity_string]");
+ AddOption("--allow-fs-read",
+ "allow permissions to read the filesystem",
+ &EnvironmentOptions::allow_fs_read,
+ kAllowedInEnvvar);
+ AddOption("--allow-fs-write",
+ "allow permissions to write in the filesystem",
+ &EnvironmentOptions::allow_fs_write,
+ kAllowedInEnvvar);
+ AddOption("--allow-child-process",
+ "allow use of child process when any permissions are set",
+ &EnvironmentOptions::allow_child_process,
+ kAllowedInEnvvar);
+ AddOption("--allow-worker",
+ "allow worker threads when any permissions are set",
+ &EnvironmentOptions::allow_worker_threads,
+ kAllowedInEnvvar);
AddOption("--experimental-repl-await",
"experimental await keyword support in REPL",
&EnvironmentOptions::experimental_repl_await,
diff --git a/src/node_options.h b/src/node_options.h
index 07ec47d861..b5a3505402 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -120,6 +120,11 @@ class EnvironmentOptions : public Options {
std::string experimental_policy;
std::string experimental_policy_integrity;
bool has_policy_integrity_string = false;
+ bool experimental_permission = false;
+ std::string allow_fs_read;
+ std::string allow_fs_write;
+ bool allow_child_process = false;
+ bool allow_worker_threads = false;
bool experimental_repl_await = true;
bool experimental_vm_modules = false;
bool expose_internals = false;
diff --git a/src/node_worker.cc b/src/node_worker.cc
index 66ab3dfd8c..8d7353a557 100644
--- a/src/node_worker.cc
+++ b/src/node_worker.cc
@@ -1,15 +1,16 @@
#include "node_worker.h"
+#include "async_wrap-inl.h"
#include "debug_utils-inl.h"
#include "histogram-inl.h"
#include "memory_tracker-inl.h"
+#include "node_buffer.h"
#include "node_errors.h"
#include "node_external_reference.h"
-#include "node_buffer.h"
#include "node_options-inl.h"
#include "node_perf.h"
#include "node_snapshot_builder.h"
+#include "permission/permission.h"
#include "util-inl.h"
-#include "async_wrap-inl.h"
#include <memory>
#include <string>
@@ -457,6 +458,8 @@ Worker::~Worker() {
void Worker::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kWorkerThreads, "");
Isolate* isolate = args.GetIsolate();
CHECK(args.IsConstructCall());
diff --git a/src/permission/child_process_permission.cc b/src/permission/child_process_permission.cc
new file mode 100644
index 0000000000..7390ea59b6
--- /dev/null
+++ b/src/permission/child_process_permission.cc
@@ -0,0 +1,27 @@
+#include "child_process_permission.h"
+
+#include <string>
+#include <vector>
+
+namespace node {
+
+namespace permission {
+
+// Currently, ChildProcess manage a single state
+// Once denied, it's always denied
+void ChildProcessPermission::Apply(const std::string& deny,
+ PermissionScope scope) {}
+
+bool ChildProcessPermission::Deny(PermissionScope perm,
+ const std::vector<std::string>& params) {
+ deny_all_ = true;
+ return true;
+}
+
+bool ChildProcessPermission::is_granted(PermissionScope perm,
+ const std::string_view& param) {
+ return deny_all_ == false;
+}
+
+} // namespace permission
+} // namespace node
diff --git a/src/permission/child_process_permission.h b/src/permission/child_process_permission.h
new file mode 100644
index 0000000000..7a8678a83c
--- /dev/null
+++ b/src/permission/child_process_permission.h
@@ -0,0 +1,30 @@
+#ifndef SRC_PERMISSION_CHILD_PROCESS_PERMISSION_H_
+#define SRC_PERMISSION_CHILD_PROCESS_PERMISSION_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include <vector>
+#include "permission/permission_base.h"
+
+namespace node {
+
+namespace permission {
+
+class ChildProcessPermission final : public PermissionBase {
+ public:
+ void Apply(const std::string& deny, PermissionScope scope) override;
+ bool Deny(PermissionScope scope,
+ const std::vector<std::string>& params) override;
+ bool is_granted(PermissionScope perm,
+ const std::string_view& param = "") override;
+
+ private:
+ bool deny_all_;
+};
+
+} // namespace permission
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_PERMISSION_CHILD_PROCESS_PERMISSION_H_
diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc
new file mode 100644
index 0000000000..7900a8445a
--- /dev/null
+++ b/src/permission/fs_permission.cc
@@ -0,0 +1,216 @@
+#include "fs_permission.h"
+#include "base_object-inl.h"
+#include "util.h"
+#include "v8.h"
+
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <algorithm>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace {
+
+std::string WildcardIfDir(const std::string& res) noexcept {
+ uv_fs_t req;
+ int rc = uv_fs_stat(nullptr, &req, res.c_str(), nullptr);
+ if (rc == 0) {
+ const uv_stat_t* const s = static_cast<const uv_stat_t*>(req.ptr);
+ if (s->st_mode & S_IFDIR) {
+ // add wildcard when directory
+ if (res.back() == node::kPathSeparator) {
+ return res + "*";
+ }
+ return res + node::kPathSeparator + "*";
+ }
+ }
+ uv_fs_req_cleanup(&req);
+ return res;
+}
+
+void FreeRecursivelyNode(
+ node::permission::FSPermission::RadixTree::Node* node) {
+ if (node == nullptr) {
+ return;
+ }
+
+ if (node->children.size()) {
+ for (auto& c : node->children) {
+ FreeRecursivelyNode(c.second);
+ }
+ }
+
+ if (node->wildcard_child != nullptr) {
+ delete node->wildcard_child;
+ }
+ delete node;
+}
+
+bool is_tree_granted(node::permission::FSPermission::RadixTree* deny_tree,
+ node::permission::FSPermission::RadixTree* granted_tree,
+ const std::string_view& param) {
+#ifdef _WIN32
+ // is UNC file path
+ if (param.rfind("\\\\", 0) == 0) {
+ // return lookup with normalized param
+ int starting_pos = 4; // "\\?\"
+ if (param.rfind("\\\\?\\UNC\\") == 0) {
+ starting_pos += 4; // "UNC\"
+ }
+ auto normalized = param.substr(starting_pos);
+ return !deny_tree->Lookup(normalized) &&
+ granted_tree->Lookup(normalized, true);
+ }
+#endif
+ return !deny_tree->Lookup(param) && granted_tree->Lookup(param, true);
+}
+
+} // namespace
+
+namespace node {
+
+namespace permission {
+
+// allow = '*'
+// allow = '/tmp/,/home/example.js'
+void FSPermission::Apply(const std::string& allow, PermissionScope scope) {
+ for (const auto& res : SplitString(allow, ',')) {
+ if (res == "*") {
+ if (scope == PermissionScope::kFileSystemRead) {
+ deny_all_in_ = false;
+ allow_all_in_ = true;
+ } else {
+ deny_all_out_ = false;
+ allow_all_out_ = true;
+ }
+ return;
+ }
+ GrantAccess(scope, res);
+ }
+}
+
+bool FSPermission::Deny(PermissionScope perm,
+ const std::vector<std::string>& params) {
+ if (perm == PermissionScope::kFileSystem) {
+ deny_all_in_ = true;
+ deny_all_out_ = true;
+ return true;
+ }
+
+ bool deny_all = params.size() == 0;
+ if (perm == PermissionScope::kFileSystemRead) {
+ if (deny_all) deny_all_in_ = true;
+ // when deny_all_in is already true permission.deny should be idempotent
+ if (deny_all_in_) return true;
+ allow_all_in_ = false;
+ for (auto& param : params) {
+ deny_in_fs_.Insert(WildcardIfDir(param));
+ }
+ return true;
+ }
+
+ if (perm == PermissionScope::kFileSystemWrite) {
+ if (deny_all) deny_all_out_ = true;
+ // when deny_all_out is already true permission.deny should be idempotent
+ if (deny_all_out_) return true;
+ allow_all_out_ = false;
+
+ for (auto& param : params) {
+ deny_out_fs_.Insert(WildcardIfDir(param));
+ }
+ return true;
+ }
+ return false;
+}
+
+void FSPermission::GrantAccess(PermissionScope perm, std::string res) {
+ const std::string path = WildcardIfDir(res);
+ if (perm == PermissionScope::kFileSystemRead) {
+ granted_in_fs_.Insert(path);
+ deny_all_in_ = false;
+ } else if (perm == PermissionScope::kFileSystemWrite) {
+ granted_out_fs_.Insert(path);
+ deny_all_out_ = false;
+ }
+}
+
+bool FSPermission::is_granted(PermissionScope perm,
+ const std::string_view& param = "") {
+ switch (perm) {
+ case PermissionScope::kFileSystem:
+ return allow_all_in_ && allow_all_out_;
+ case PermissionScope::kFileSystemRead:
+ return !deny_all_in_ &&
+ ((param.empty() && allow_all_in_) || allow_all_in_ ||
+ is_tree_granted(&deny_in_fs_, &granted_in_fs_, param));
+ case PermissionScope::kFileSystemWrite:
+ return !deny_all_out_ &&
+ ((param.empty() && allow_all_out_) || allow_all_out_ ||
+ is_tree_granted(&deny_out_fs_, &granted_out_fs_, param));
+ default:
+ return false;
+ }
+}
+
+FSPermission::RadixTree::RadixTree() : root_node_(new Node("")) {}
+
+FSPermission::RadixTree::~RadixTree() {
+ FreeRecursivelyNode(root_node_);
+}
+
+bool FSPermission::RadixTree::Lookup(const std::string_view& s,
+ bool when_empty_return = false) {
+ FSPermission::RadixTree::Node* current_node = root_node_;
+ if (current_node->children.size() == 0) {
+ return when_empty_return;
+ }
+
+ unsigned int parent_node_prefix_len = current_node->prefix.length();
+ const std::string path(s);
+ auto path_len = path.length();
+
+ while (true) {
+ if (parent_node_prefix_len == path_len && current_node->IsEndNode()) {
+ return true;
+ }
+
+ auto node = current_node->NextNode(path, parent_node_prefix_len);
+ if (node == nullptr) {
+ return false;
+ }
+
+ current_node = node;
+ parent_node_prefix_len += current_node->prefix.length();
+ if (current_node->wildcard_child != nullptr &&
+ path_len >= (parent_node_prefix_len - 2 /* slash* */)) {
+ return true;
+ }
+ }
+}
+
+void FSPermission::RadixTree::Insert(const std::string& path) {
+ FSPermission::RadixTree::Node* current_node = root_node_;
+
+ unsigned int parent_node_prefix_len = current_node->prefix.length();
+ int path_len = path.length();
+
+ for (int i = 1; i <= path_len; ++i) {
+ bool is_wildcard_node = path[i - 1] == '*';
+ bool is_last_char = i == path_len;
+
+ if (is_wildcard_node || is_last_char) {
+ std::string node_path = path.substr(parent_node_prefix_len, i);
+ current_node = current_node->CreateChild(node_path);
+ }
+
+ if (is_wildcard_node) {
+ current_node = current_node->CreateWildcardChild();
+ parent_node_prefix_len = i;
+ }
+ }
+}
+
+} // namespace permission
+} // namespace node
diff --git a/src/permission/fs_permission.h b/src/permission/fs_permission.h
new file mode 100644
index 0000000000..0038496d02
--- /dev/null
+++ b/src/permission/fs_permission.h
@@ -0,0 +1,163 @@
+#ifndef SRC_PERMISSION_FS_PERMISSION_H_
+#define SRC_PERMISSION_FS_PERMISSION_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "v8.h"
+
+#include <unordered_map>
+#include <vector>
+#include "permission/permission_base.h"
+#include "util.h"
+
+namespace node {
+
+namespace permission {
+
+class FSPermission final : public PermissionBase {
+ public:
+ void Apply(const std::string& deny, PermissionScope scope) override;
+ bool Deny(PermissionScope scope,
+ const std::vector<std::string>& params) override;
+ bool is_granted(PermissionScope perm, const std::string_view& param) override;
+
+ // For debugging purposes, use the gist function to print the whole tree
+ // https://gist.github.com/RafaelGSS/5b4f09c559a54f53f9b7c8c030744d19
+ struct RadixTree {
+ struct Node {
+ std::string prefix;
+ std::unordered_map<char, Node*> children;
+ Node* wildcard_child;
+
+ explicit Node(const std::string& pre)
+ : prefix(pre), wildcard_child(nullptr) {}
+
+ Node() : wildcard_child(nullptr) {}
+
+ Node* CreateChild(std::string prefix) {
+ char label = prefix[0];
+
+ Node* child = children[label];
+ if (child == nullptr) {
+ children[label] = new Node(prefix);
+ return children[label];
+ }
+
+ // swap prefix
+ unsigned int i = 0;
+ unsigned int prefix_len = prefix.length();
+ for (; i < child->prefix.length(); ++i) {
+ if (i > prefix_len || prefix[i] != child->prefix[i]) {
+ std::string parent_prefix = child->prefix.substr(0, i);
+ std::string child_prefix = child->prefix.substr(i);
+
+ child->prefix = child_prefix;
+ Node* split_child = new Node(parent_prefix);
+ split_child->children[child_prefix[0]] = child;
+ children[parent_prefix[0]] = split_child;
+
+ return split_child->CreateChild(prefix.substr(i));
+ }
+ }
+ return child->CreateChild(prefix.substr(i));
+ }
+
+ Node* CreateWildcardChild() {
+ if (wildcard_child != nullptr) {
+ return wildcard_child;
+ }
+ wildcard_child = new Node();
+ return wildcard_child;
+ }
+
+ Node* NextNode(const std::string& path, unsigned int idx) {
+ if (idx >= path.length()) {
+ return nullptr;
+ }
+
+ auto it = children.find(path[idx]);
+ if (it == children.end()) {
+ return nullptr;
+ }
+ auto child = it->second;
+ // match prefix
+ unsigned int prefix_len = child->prefix.length();
+ for (unsigned int i = 0; i < path.length(); ++i) {
+ if (i >= prefix_len || child->prefix[i] == '*') {
+ return child;
+ }
+
+ // Handle optional trailing
+ // path = /home/subdirectory
+ // child = subdirectory/*
+ if (idx >= path.length() &&
+ child->prefix[i] == node::kPathSeparator) {
+ continue;
+ }
+
+ if (path[idx++] != child->prefix[i]) {
+ return nullptr;
+ }
+ }
+ return child;
+ }
+
+ // A node can be a *end* node and have children
+ // E.g: */slower*, */slown* are inserted:
+ // /slow
+ // ---> er
+ // ---> n
+ // If */slow* is inserted right after, it will create an
+ // empty node
+ // /slow
+ // ---> '\000' ASCII (0) || \0
+ // ---> er
+ // ---> n
+ bool IsEndNode() {
+ if (children.size() == 0) {
+ return true;
+ }
+ return children['\0'] != nullptr;
+ }
+ };
+
+ RadixTree();
+ ~RadixTree();
+ void Insert(const std::string& s);
+ bool Lookup(const std::string_view& s) { return Lookup(s, false); }
+ bool Lookup(const std::string_view& s, bool when_empty_return);
+
+ private:
+ Node* root_node_;
+ };
+
+ private:
+ void GrantAccess(PermissionScope scope, std::string param);
+ void RestrictAccess(PermissionScope scope,
+ const std::vector<std::string>& params);
+ // /tmp/* --grant
+ // /tmp/dsadsa/t.js denied in runtime
+ //
+ // /tmp/text.txt -- grant
+ // /tmp/text.txt -- denied in runtime
+ //
+ // fs granted on startup
+ RadixTree granted_in_fs_;
+ RadixTree granted_out_fs_;
+ // fs denied in runtime
+ RadixTree deny_in_fs_;
+ RadixTree deny_out_fs_;
+
+ bool deny_all_in_ = true;
+ bool deny_all_out_ = true;
+
+ bool allow_all_in_ = false;
+ bool allow_all_out_ = false;
+};
+
+} // namespace permission
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_PERMISSION_FS_PERMISSION_H_
diff --git a/src/permission/permission.cc b/src/permission/permission.cc
new file mode 100644
index 0000000000..c156133e64
--- /dev/null
+++ b/src/permission/permission.cc
@@ -0,0 +1,200 @@
+#include "permission.h"
+#include "base_object-inl.h"
+#include "env-inl.h"
+#include "memory_tracker-inl.h"
+#include "node.h"
+#include "node_errors.h"
+#include "node_external_reference.h"
+
+#include "v8.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace node {
+
+using v8::Array;
+using v8::Context;
+using v8::FunctionCallbackInfo;
+using v8::Integer;
+using v8::Local;
+using v8::Object;
+using v8::String;
+using v8::Value;
+
+namespace permission {
+
+namespace {
+
+// permission.deny('fs.read', ['/tmp/'])
+// permission.deny('fs.read')
+static void Deny(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ v8::Isolate* isolate = env->isolate();
+ CHECK(args[0]->IsString());
+ std::string deny_scope = *String::Utf8Value(isolate, args[0]);
+ PermissionScope scope = Permission::StringToPermission(deny_scope);
+ if (scope == PermissionScope::kPermissionsRoot) {
+ return args.GetReturnValue().Set(false);
+ }
+
+ std::vector<std::string> params;
+ if (args.Length() == 1 || args[1]->IsUndefined()) {
+ return args.GetReturnValue().Set(env->permission()->Deny(scope, params));
+ }
+
+ CHECK(args[1]->IsArray());
+ Local<Array> js_params = Local<Array>::Cast(args[1]);
+ Local<Context> context = isolate->GetCurrentContext();
+
+ for (uint32_t i = 0; i < js_params->Length(); ++i) {
+ Local<Value> arg;
+ if (!js_params->Get(context, Integer::New(isolate, i)).ToLocal(&arg)) {
+ return;
+ }
+ String::Utf8Value utf8_arg(isolate, arg);
+ if (*utf8_arg == nullptr) {
+ return;
+ }
+ params.push_back(*utf8_arg);
+ }
+
+ return args.GetReturnValue().Set(env->permission()->Deny(scope, params));
+}
+
+// permission.has('fs.in', '/tmp/')
+// permission.has('fs.in')
+static void Has(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ v8::Isolate* isolate = env->isolate();
+ CHECK(args[0]->IsString());
+
+ String::Utf8Value utf8_deny_scope(isolate, args[0]);
+ if (*utf8_deny_scope == nullptr) {
+ return;
+ }
+
+ const std::string deny_scope = *utf8_deny_scope;
+ PermissionScope scope = Permission::StringToPermission(deny_scope);
+ if (scope == PermissionScope::kPermissionsRoot) {
+ return args.GetReturnValue().Set(false);
+ }
+
+ if (args.Length() > 1 && !args[1]->IsUndefined()) {
+ String::Utf8Value utf8_arg(isolate, args[1]);
+ if (*utf8_arg == nullptr) {
+ return;
+ }
+ return args.GetReturnValue().Set(
+ env->permission()->is_granted(scope, *utf8_arg));
+ }
+
+ return args.GetReturnValue().Set(env->permission()->is_granted(scope));
+}
+
+} // namespace
+
+#define V(Name, label, _) \
+ if (perm == PermissionScope::k##Name) return #Name;
+const char* Permission::PermissionToString(const PermissionScope perm) {
+ PERMISSIONS(V)
+ return nullptr;
+}
+#undef V
+
+#define V(Name, label, _) \
+ if (perm == label) return PermissionScope::k##Name;
+PermissionScope Permission::StringToPermission(const std::string& perm) {
+ PERMISSIONS(V)
+ return PermissionScope::kPermissionsRoot;
+}
+#undef V
+
+Permission::Permission() : enabled_(false) {
+ std::shared_ptr<PermissionBase> fs = std::make_shared<FSPermission>();
+ std::shared_ptr<PermissionBase> child_p =
+ std::make_shared<ChildProcessPermission>();
+ std::shared_ptr<PermissionBase> worker_t =
+ std::make_shared<WorkerPermission>();
+#define V(Name, _, __) \
+ nodes_.insert(std::make_pair(PermissionScope::k##Name, fs));
+ FILESYSTEM_PERMISSIONS(V)
+#undef V
+#define V(Name, _, __) \
+ nodes_.insert(std::make_pair(PermissionScope::k##Name, child_p));
+ CHILD_PROCESS_PERMISSIONS(V)
+#undef V
+#define V(Name, _, __) \
+ nodes_.insert(std::make_pair(PermissionScope::k##Name, worker_t));
+ WORKER_THREADS_PERMISSIONS(V)
+#undef V
+}
+
+void Permission::ThrowAccessDenied(Environment* env,
+ PermissionScope perm,
+ const std::string_view& res) {
+ Local<Value> err = ERR_ACCESS_DENIED(env->isolate());
+ CHECK(err->IsObject());
+ err.As<Object>()
+ ->Set(env->context(),
+ env->permission_string(),
+ v8::String::NewFromUtf8(env->isolate(),
+ PermissionToString(perm),
+ v8::NewStringType::kNormal)
+ .ToLocalChecked())
+ .FromMaybe(false);
+ err.As<Object>()
+ ->Set(env->context(),
+ env->resource_string(),
+ v8::String::NewFromUtf8(env->isolate(),
+ std::string(res).c_str(),
+ v8::NewStringType::kNormal)
+ .ToLocalChecked())
+ .FromMaybe(false);
+ env->isolate()->ThrowException(err);
+}
+
+void Permission::EnablePermissions() {
+ if (!enabled_) {
+ enabled_ = true;
+ }
+}
+
+void Permission::Apply(const std::string& allow, PermissionScope scope) {
+ auto permission = nodes_.find(scope);
+ if (permission != nodes_.end()) {
+ permission->second->Apply(allow, scope);
+ }
+}
+
+bool Permission::Deny(PermissionScope scope,
+ const std::vector<std::string>& params) {
+ auto permission = nodes_.find(scope);
+ if (permission != nodes_.end()) {
+ return permission->second->Deny(scope, params);
+ }
+ return false;
+}
+
+void Initialize(Local<Object> target,
+ Local<Value> unused,
+ Local<Context> context,
+ void* priv) {
+ SetMethod(context, target, "deny", Deny);
+ SetMethodNoSideEffect(context, target, "has", Has);
+
+ target->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen).FromJust();
+}
+
+void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
+ registry->Register(Deny);
+ registry->Register(Has);
+}
+
+} // namespace permission
+} // namespace node
+
+NODE_BINDING_CONTEXT_AWARE_INTERNAL(permission, node::permission::Initialize)
+NODE_BINDING_EXTERNAL_REFERENCE(permission,
+ node::permission::RegisterExternalReferences)
diff --git a/src/permission/permission.h b/src/permission/permission.h
new file mode 100644
index 0000000000..f5b6f4cba9
--- /dev/null
+++ b/src/permission/permission.h
@@ -0,0 +1,73 @@
+#ifndef SRC_PERMISSION_PERMISSION_H_
+#define SRC_PERMISSION_PERMISSION_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "debug_utils.h"
+#include "node_options.h"
+#include "permission/child_process_permission.h"
+#include "permission/fs_permission.h"
+#include "permission/permission_base.h"
+#include "permission/worker_permission.h"
+#include "v8.h"
+
+#include <string_view>
+#include <unordered_map>
+
+namespace node {
+
+class Environment;
+
+namespace permission {
+
+#define THROW_IF_INSUFFICIENT_PERMISSIONS(env, perm_, resource_, ...) \
+ do { \
+ if (UNLIKELY(!(env)->permission()->is_granted(perm_, resource_))) { \
+ node::permission::Permission::ThrowAccessDenied( \
+ (env), perm_, resource_); \
+ return __VA_ARGS__; \
+ } \
+ } while (0)
+
+class Permission {
+ public:
+ Permission();
+
+ FORCE_INLINE bool is_granted(const PermissionScope permission,
+ const std::string_view& res = "") const {
+ if (LIKELY(!enabled_)) return true;
+ return is_scope_granted(permission, res);
+ }
+
+ static PermissionScope StringToPermission(const std::string& perm);
+ static const char* PermissionToString(PermissionScope perm);
+ static void ThrowAccessDenied(Environment* env,
+ PermissionScope perm,
+ const std::string_view& res);
+
+ // CLI Call
+ void Apply(const std::string& deny, PermissionScope scope);
+ // Permission.Deny API
+ bool Deny(PermissionScope scope, const std::vector<std::string>& params);
+ void EnablePermissions();
+
+ private:
+ COLD_NOINLINE bool is_scope_granted(const PermissionScope permission,
+ const std::string_view& res = "") const {
+ auto perm_node = nodes_.find(permission);
+ if (perm_node != nodes_.end()) {
+ return perm_node->second->is_granted(permission, res);
+ }
+ return false;
+ }
+
+ std::unordered_map<PermissionScope, std::shared_ptr<PermissionBase>> nodes_;
+ bool enabled_;
+};
+
+} // namespace permission
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_PERMISSION_PERMISSION_H_
diff --git a/src/permission/permission_base.h b/src/permission/permission_base.h
new file mode 100644
index 0000000000..4240db17cd
--- /dev/null
+++ b/src/permission/permission_base.h
@@ -0,0 +1,51 @@
+#ifndef SRC_PERMISSION_PERMISSION_BASE_H_
+#define SRC_PERMISSION_PERMISSION_BASE_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include <map>
+#include <string>
+#include <string_view>
+#include "v8.h"
+
+namespace node {
+
+namespace permission {
+
+#define FILESYSTEM_PERMISSIONS(V) \
+ V(FileSystem, "fs", PermissionsRoot) \
+ V(FileSystemRead, "fs.read", FileSystem) \
+ V(FileSystemWrite, "fs.write", FileSystem)
+
+#define CHILD_PROCESS_PERMISSIONS(V) V(ChildProcess, "child", PermissionsRoot)
+
+#define WORKER_THREADS_PERMISSIONS(V) \
+ V(WorkerThreads, "worker", PermissionsRoot)
+
+#define PERMISSIONS(V) \
+ FILESYSTEM_PERMISSIONS(V) \
+ CHILD_PROCESS_PERMISSIONS(V) \
+ WORKER_THREADS_PERMISSIONS(V)
+
+#define V(name, _, __) k##name,
+enum class PermissionScope {
+ kPermissionsRoot = -1,
+ PERMISSIONS(V) kPermissionsCount
+};
+#undef V
+
+class PermissionBase {
+ public:
+ virtual void Apply(const std::string& deny, PermissionScope scope) = 0;
+ virtual bool Deny(PermissionScope scope,
+ const std::vector<std::string>& params) = 0;
+ virtual bool is_granted(PermissionScope perm,
+ const std::string_view& param = "") = 0;
+};
+
+} // namespace permission
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_PERMISSION_PERMISSION_BASE_H_
diff --git a/src/permission/worker_permission.cc b/src/permission/worker_permission.cc
new file mode 100644
index 0000000000..2de48e1ba7
--- /dev/null
+++ b/src/permission/worker_permission.cc
@@ -0,0 +1,26 @@
+#include "permission/worker_permission.h"
+
+#include <string>
+#include <vector>
+
+namespace node {
+
+namespace permission {
+
+// Currently, PolicyDenyWorker manage a single state
+// Once denied, it's always denied
+void WorkerPermission::Apply(const std::string& deny, PermissionScope scope) {}
+
+bool WorkerPermission::Deny(PermissionScope perm,
+ const std::vector<std::string>& params) {
+ deny_all_ = true;
+ return true;
+}
+
+bool WorkerPermission::is_granted(PermissionScope perm,
+ const std::string_view& param) {
+ return deny_all_ == false;
+}
+
+} // namespace permission
+} // namespace node
diff --git a/src/permission/worker_permission.h b/src/permission/worker_permission.h
new file mode 100644
index 0000000000..1a93e2253d
--- /dev/null
+++ b/src/permission/worker_permission.h
@@ -0,0 +1,30 @@
+#ifndef SRC_PERMISSION_WORKER_PERMISSION_H_
+#define SRC_PERMISSION_WORKER_PERMISSION_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include <vector>
+#include "permission/permission_base.h"
+
+namespace node {
+
+namespace permission {
+
+class WorkerPermission final : public PermissionBase {
+ public:
+ void Apply(const std::string& deny, PermissionScope scope) override;
+ bool Deny(PermissionScope scope,
+ const std::vector<std::string>& params) override;
+ bool is_granted(PermissionScope perm,
+ const std::string_view& param = "") override;
+
+ private:
+ bool deny_all_;
+};
+
+} // namespace permission
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_PERMISSION_WORKER_PERMISSION_H_
diff --git a/src/process_wrap.cc b/src/process_wrap.cc
index ffa5dbd130..42a746308b 100644
--- a/src/process_wrap.cc
+++ b/src/process_wrap.cc
@@ -20,6 +20,7 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "env-inl.h"
+#include "permission/permission.h"
#include "stream_base-inl.h"
#include "stream_wrap.h"
#include "util-inl.h"
@@ -147,6 +148,8 @@ class ProcessWrap : public HandleWrap {
Local<Context> context = env->context();
ProcessWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kChildProcess, "");
Local<Object> js_options =
args[0]->ToObject(env->context()).ToLocalChecked();
diff --git a/src/util.h b/src/util.h
index 9be4aef568..e30f298f8d 100644
--- a/src/util.h
+++ b/src/util.h
@@ -538,6 +538,11 @@ class Utf8Value : public MaybeStackBuffer<char> {
public:
explicit Utf8Value(v8::Isolate* isolate, v8::Local<v8::Value> value);
+ inline std::string ToString() const { return std::string(out(), length()); }
+ inline std::string_view ToStringView() const {
+ return std::string_view(out(), length());
+ }
+
inline bool operator==(const char* a) const {
return strcmp(out(), a) == 0;
}
@@ -553,6 +558,9 @@ class BufferValue : public MaybeStackBuffer<char> {
explicit BufferValue(v8::Isolate* isolate, v8::Local<v8::Value> value);
inline std::string ToString() const { return std::string(out(), length()); }
+ inline std::string_view ToStringView() const {
+ return std::string_view(out(), length());
+ }
};
#define SPREAD_BUFFER_ARG(val, name) \
diff --git a/test/addons/no-addons/permission.js b/test/addons/no-addons/permission.js
new file mode 100644
index 0000000000..0fbcd2bb1e
--- /dev/null
+++ b/test/addons/no-addons/permission.js
@@ -0,0 +1,43 @@
+// Flags: --experimental-permission --allow-fs-read=*
+
+'use strict';
+
+const common = require('../../common');
+const assert = require('assert');
+
+const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
+
+const assertError = (error) => {
+ assert(error instanceof Error);
+ assert.strictEqual(error.code, 'ERR_DLOPEN_DISABLED');
+ assert.strictEqual(
+ error.message,
+ 'Cannot load native addon because loading addons is disabled.',
+ );
+};
+
+{
+ let threw = false;
+
+ try {
+ require(bindingPath);
+ } catch (error) {
+ assertError(error);
+ threw = true;
+ }
+
+ assert(threw);
+}
+
+{
+ let threw = false;
+
+ try {
+ process.dlopen({ exports: {} }, bindingPath);
+ } catch (error) {
+ assertError(error);
+ threw = true;
+ }
+
+ assert(threw);
+}
diff --git a/test/common/README.md b/test/common/README.md
index 4d26e355d5..061c1e6627 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -1005,9 +1005,12 @@ The `tmpdir` module supports the use of a temporary directory for testing.
The realpath of the testing temporary directory.
-### `refresh()`
+### `refresh(useSpawn)`
-Deletes and recreates the testing temporary directory.
+* `useSpawn` [\<boolean>][<boolean>] default = false
+
+Deletes and recreates the testing temporary directory. When `useSpawn` is true
+this action is performed using `child_process.spawnSync`.
The first time `refresh()` runs, it adds a listener to process `'exit'` that
cleans the temporary directory. Thus, every file under `tmpdir.path` needs to
diff --git a/test/common/tmpdir.js b/test/common/tmpdir.js
index 1f2ebb18ee..3c4ca546d0 100644
--- a/test/common/tmpdir.js
+++ b/test/common/tmpdir.js
@@ -1,11 +1,23 @@
'use strict';
+const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { isMainThread } = require('worker_threads');
-function rmSync(pathname) {
- fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true });
+function rmSync(pathname, useSpawn) {
+ if (useSpawn) {
+ const escapedPath = pathname.replaceAll('\\', '\\\\');
+ spawnSync(
+ process.execPath,
+ [
+ '-e',
+ `require("fs").rmSync("${escapedPath}", { maxRetries: 3, recursive: true, force: true });`,
+ ],
+ );
+ } else {
+ fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true });
+ }
}
const testRoot = process.env.NODE_TEST_DIR ?
@@ -18,25 +30,27 @@ const tmpdirName = '.tmp.' +
const tmpPath = path.join(testRoot, tmpdirName);
let firstRefresh = true;
-function refresh() {
- rmSync(tmpPath);
+function refresh(useSpawn = false) {
+ rmSync(tmpPath, useSpawn);
fs.mkdirSync(tmpPath);
if (firstRefresh) {
firstRefresh = false;
// Clean only when a test uses refresh. This allows for child processes to
// use the tmpdir and only the parent will clean on exit.
- process.on('exit', onexit);
+ process.on('exit', () => {
+ return onexit(useSpawn);
+ });
}
}
-function onexit() {
+function onexit(useSpawn) {
// Change directory to avoid possible EBUSY
if (isMainThread)
process.chdir(testRoot);
try {
- rmSync(tmpPath);
+ rmSync(tmpPath, useSpawn);
} catch (e) {
console.error('Can\'t clean tmpdir:', tmpPath);
diff --git a/test/fixtures/permission/deny/protected-file.md b/test/fixtures/permission/deny/protected-file.md
new file mode 100644
index 0000000000..845763d240
--- /dev/null
+++ b/test/fixtures/permission/deny/protected-file.md
@@ -0,0 +1,3 @@
+# Protected File
+
+Example of a protected file to be used in the PolicyDenyFs module
diff --git a/test/fixtures/permission/deny/protected-folder/protected-file.md b/test/fixtures/permission/deny/protected-folder/protected-file.md
new file mode 100644
index 0000000000..845763d240
--- /dev/null
+++ b/test/fixtures/permission/deny/protected-folder/protected-file.md
@@ -0,0 +1,3 @@
+# Protected File
+
+Example of a protected file to be used in the PolicyDenyFs module
diff --git a/test/fixtures/permission/deny/regular-file.md b/test/fixtures/permission/deny/regular-file.md
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/test/fixtures/permission/deny/regular-file.md
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index c0fd99055d..6efb806e20 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -49,6 +49,7 @@ const expectedModules = new Set([
'NativeModule internal/constants',
'NativeModule path',
'NativeModule internal/process/execution',
+ 'NativeModule internal/process/permission',
'NativeModule internal/process/warning',
'NativeModule internal/console/constructor',
'NativeModule internal/console/global',
@@ -59,6 +60,7 @@ const expectedModules = new Set([
'NativeModule internal/url',
'NativeModule util',
'Internal Binding performance',
+ 'Internal Binding permission',
'NativeModule internal/perf/utils',
'NativeModule internal/event_target',
'Internal Binding mksnapshot',
diff --git a/test/parallel/test-cli-bad-options.js b/test/parallel/test-cli-bad-options.js
index 1bdaf5ee93..8a77e94bab 100644
--- a/test/parallel/test-cli-bad-options.js
+++ b/test/parallel/test-cli-bad-options.js
@@ -14,6 +14,17 @@ if (process.features.inspector) {
}
requiresArgument('--eval');
+missingOption('--allow-fs-read=*', '--experimental-permission');
+missingOption('--allow-fs-write=*', '--experimental-permission');
+
+function missingOption(option, requiredOption) {
+ const r = spawnSync(process.execPath, [option], { encoding: 'utf8' });
+ assert.strictEqual(r.status, 1);
+
+ const message = `${requiredOption} is required`;
+ assert.match(r.stderr, new RegExp(message));
+}
+
function requiresArgument(option) {
const r = spawnSync(process.execPath, [option], { encoding: 'utf8' });
diff --git a/test/parallel/test-cli-permission-deny-fs.js b/test/parallel/test-cli-permission-deny-fs.js
new file mode 100644
index 0000000000..6af6ba4078
--- /dev/null
+++ b/test/parallel/test-cli-permission-deny-fs.js
@@ -0,0 +1,128 @@
+'use strict';
+
+require('../common');
+const { spawnSync } = require('child_process');
+const assert = require('assert');
+const fs = require('fs');
+
+{
+ const { status, stdout } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission', '-e',
+ `console.log(process.permission.has("fs"));
+ console.log(process.permission.has("fs.read"));
+ console.log(process.permission.has("fs.write"));`,
+ ]
+ );
+
+ const [fs, fsIn, fsOut] = stdout.toString().split('\n');
+ assert.strictEqual(fs, 'false');
+ assert.strictEqual(fsIn, 'false');
+ assert.strictEqual(fsOut, 'false');
+ assert.strictEqual(status, 0);
+}
+
+{
+ const { status, stdout } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-write', '/tmp/', '-e',
+ `console.log(process.permission.has("fs"));
+ console.log(process.permission.has("fs.read"));
+ console.log(process.permission.has("fs.write"));
+ console.log(process.permission.has("fs.write", "/tmp/"));`,
+ ]
+ );
+ const [fs, fsIn, fsOut, fsOutAllowed] = stdout.toString().split('\n');
+ assert.strictEqual(fs, 'false');
+ assert.strictEqual(fsIn, 'false');
+ assert.strictEqual(fsOut, 'false');
+ assert.strictEqual(fsOutAllowed, 'true');
+ assert.strictEqual(status, 0);
+}
+
+{
+ const { status, stdout } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-write', '*', '-e',
+ `console.log(process.permission.has("fs"));
+ console.log(process.permission.has("fs.read"));
+ console.log(process.permission.has("fs.write"));`,
+ ]
+ );
+
+ const [fs, fsIn, fsOut] = stdout.toString().split('\n');
+ assert.strictEqual(fs, 'false');
+ assert.strictEqual(fsIn, 'false');
+ assert.strictEqual(fsOut, 'true');
+ assert.strictEqual(status, 0);
+}
+
+{
+ const { status, stdout } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-read', '*', '-e',
+ `console.log(process.permission.has("fs"));
+ console.log(process.permission.has("fs.read"));
+ console.log(process.permission.has("fs.write"));`,
+ ]
+ );
+
+ const [fs, fsIn, fsOut] = stdout.toString().split('\n');
+ assert.strictEqual(fs, 'false');
+ assert.strictEqual(fsIn, 'true');
+ assert.strictEqual(fsOut, 'false');
+ assert.strictEqual(status, 0);
+}
+
+{
+ const { status, stderr } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-write=*', '-p',
+ 'fs.readFileSync(process.execPath)',
+ ]
+ );
+ assert.ok(
+ stderr.toString().includes('Access to this API has been restricted'),
+ stderr);
+ assert.strictEqual(status, 1);
+}
+
+{
+ const { status, stderr } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '-p',
+ 'fs.readFileSync(process.execPath)',
+ ]
+ );
+ assert.ok(
+ stderr.toString().includes('Access to this API has been restricted'),
+ stderr);
+ assert.strictEqual(status, 1);
+}
+
+{
+ const { status, stderr } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-read=*', '-p',
+ 'fs.writeFileSync("policy-deny-example.md", "# test")',
+ ]
+ );
+ assert.ok(
+ stderr.toString().includes('Access to this API has been restricted'),
+ stderr);
+ assert.strictEqual(status, 1);
+ assert.ok(!fs.existsSync('permission-deny-example.md'));
+}
diff --git a/test/parallel/test-permission-deny-allow-child-process-cli.js b/test/parallel/test-permission-deny-allow-child-process-cli.js
new file mode 100644
index 0000000000..6cffc19719
--- /dev/null
+++ b/test/parallel/test-permission-deny-allow-child-process-cli.js
@@ -0,0 +1,26 @@
+// Flags: --experimental-permission --allow-child-process --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const assert = require('assert');
+const childProcess = require('child_process');
+
+if (process.argv[2] === 'child') {
+ process.exit(0);
+}
+
+// Guarantee the initial state
+{
+ assert.ok(process.permission.has('child'));
+}
+
+// When a permission is set by cli, the process shouldn't be able
+// to spawn unless --allow-child-process is sent
+{
+ // doesNotThrow
+ childProcess.spawnSync(process.execPath, ['--version']);
+ childProcess.execSync(process.execPath, ['--version']);
+ childProcess.fork(__filename, ['child']);
+ childProcess.execFileSync(process.execPath, ['--version']);
+}
diff --git a/test/parallel/test-permission-deny-allow-worker-cli.js b/test/parallel/test-permission-deny-allow-worker-cli.js
new file mode 100644
index 0000000000..ae5a28fdae
--- /dev/null
+++ b/test/parallel/test-permission-deny-allow-worker-cli.js
@@ -0,0 +1,22 @@
+// Flags: --experimental-permission --allow-worker --allow-fs-read=*
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const { isMainThread, Worker } = require('worker_threads');
+
+if (!isMainThread) {
+ process.exit(0);
+}
+
+// Guarantee the initial state
+{
+ assert.ok(process.permission.has('worker'));
+}
+
+// When a permission is set by cli, the process shouldn't be able
+// to spawn unless --allow-worker is sent
+{
+ // doesNotThrow
+ new Worker(__filename).on('exit', (code) => assert.strictEqual(code, 0));
+}
diff --git a/test/parallel/test-permission-deny-child-process-cli.js b/test/parallel/test-permission-deny-child-process-cli.js
new file mode 100644
index 0000000000..7f15cacd0d
--- /dev/null
+++ b/test/parallel/test-permission-deny-child-process-cli.js
@@ -0,0 +1,45 @@
+// Flags: --experimental-permission --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const assert = require('assert');
+const childProcess = require('child_process');
+
+if (process.argv[2] === 'child') {
+ process.exit(0);
+}
+
+// Guarantee the initial state
+{
+ assert.ok(!process.permission.has('child'));
+}
+
+// When a permission is set by cli, the process shouldn't be able
+// to spawn
+{
+ assert.throws(() => {
+ childProcess.spawn(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.exec(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.fork(__filename, ['child']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.execFile(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+}
diff --git a/test/parallel/test-permission-deny-child-process.js b/test/parallel/test-permission-deny-child-process.js
new file mode 100644
index 0000000000..36c0e9da86
--- /dev/null
+++ b/test/parallel/test-permission-deny-child-process.js
@@ -0,0 +1,52 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-child-process
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const assert = require('assert');
+const childProcess = require('child_process');
+
+if (process.argv[2] === 'child') {
+ process.exit(0);
+}
+
+{
+ // doesNotThrow
+ const spawn = childProcess.spawn(process.execPath, ['--version']);
+ spawn.kill();
+ const exec = childProcess.exec(process.execPath, ['--version']);
+ exec.kill();
+ const fork = childProcess.fork(__filename, ['child']);
+ fork.kill();
+ const execFile = childProcess.execFile(process.execPath, ['--version']);
+ execFile.kill();
+
+ assert.ok(process.permission.deny('child'));
+
+ // When a permission is set by API, the process shouldn't be able
+ // to spawn
+ assert.throws(() => {
+ childProcess.spawn(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.exec(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.fork(__filename, ['child']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+ assert.throws(() => {
+ childProcess.execFile(process.execPath, ['--version']);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'ChildProcess',
+ }));
+}
diff --git a/test/parallel/test-permission-deny-fs-read.js b/test/parallel/test-permission-deny-fs-read.js
new file mode 100644
index 0000000000..0f918acffd
--- /dev/null
+++ b/test/parallel/test-permission-deny-fs-read.js
@@ -0,0 +1,328 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const assert = require('assert');
+const fs = require('fs');
+const fixtures = require('../common/fixtures');
+const tmpdir = require('../common/tmpdir');
+const path = require('path');
+const os = require('os');
+
+const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');
+const relativeProtectedFile = './test/fixtures/permission/deny/protected-file.md';
+const absoluteProtectedFile = path.resolve(relativeProtectedFile);
+const blockedFolder = tmpdir.path;
+const regularFile = __filename;
+const uid = os.userInfo().uid;
+const gid = os.userInfo().gid;
+
+{
+ tmpdir.refresh();
+ assert.ok(process.permission.deny('fs.read', [blockedFile, blockedFolder]));
+}
+
+// fs.readFile
+{
+ assert.throws(() => {
+ fs.readFile(blockedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.readFile(relativeProtectedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.readFile(path.join(blockedFolder, 'anyfile'), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+
+ // doesNotThrow
+ fs.readFile(regularFile, () => {});
+}
+
+// fs.createReadStream
+{
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createReadStream(blockedFile);
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ })).then(common.mustCall());
+
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createReadStream(relativeProtectedFile);
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ })).then(common.mustCall());
+
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createReadStream(blockedFile);
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ })).then(common.mustCall());
+}
+
+// fs.stat
+{
+ assert.throws(() => {
+ fs.stat(blockedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.stat(relativeProtectedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.stat(path.join(blockedFolder, 'anyfile'), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+
+ // doesNotThrow
+ fs.stat(regularFile, (err) => {
+ assert.ifError(err);
+ });
+}
+
+// fs.access
+{
+ assert.throws(() => {
+ fs.access(blockedFile, fs.constants.R_OK, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.access(relativeProtectedFile, fs.constants.R_OK, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.access(path.join(blockedFolder, 'anyfile'), fs.constants.R_OK, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+
+ // doesNotThrow
+ fs.access(regularFile, fs.constants.R_OK, (err) => {
+ assert.ifError(err);
+ });
+}
+
+// fs.chownSync (should not bypass)
+{
+ assert.throws(() => {
+ // This operation will work fine
+ fs.chownSync(blockedFile, uid, gid);
+ fs.readFileSync(blockedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ // This operation will work fine
+ fs.chownSync(relativeProtectedFile, uid, gid);
+ fs.readFileSync(relativeProtectedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+}
+
+// fs.copyFile
+{
+ assert.throws(() => {
+ fs.copyFile(blockedFile, path.join(blockedFolder, 'any-other-file'), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.copyFile(relativeProtectedFile, path.join(blockedFolder, 'any-other-file'), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.copyFile(blockedFile, path.join(__dirname, 'any-other-file'), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+}
+
+// fs.cp
+{
+ assert.throws(() => {
+ fs.cpSync(blockedFile, path.join(blockedFolder, 'any-other-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ // cpSync calls statSync before reading blockedFile
+ resource: path.toNamespacedPath(blockedFolder),
+ }));
+ assert.throws(() => {
+ fs.cpSync(relativeProtectedFile, path.join(blockedFolder, 'any-other-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFolder),
+ }));
+ assert.throws(() => {
+ fs.cpSync(blockedFile, path.join(__dirname, 'any-other-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+}
+
+// fs.open
+{
+ assert.throws(() => {
+ fs.open(blockedFile, 'r', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.open(relativeProtectedFile, 'r', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.open(path.join(blockedFolder, 'anyfile'), 'r', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+
+ // doesNotThrow
+ fs.open(regularFile, 'r', (err) => {
+ assert.ifError(err);
+ });
+}
+
+// fs.opendir
+{
+ assert.throws(() => {
+ fs.opendir(blockedFolder, (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFolder),
+ }));
+ // doesNotThrow
+ fs.opendir(__dirname, (err, dir) => {
+ assert.ifError(err);
+ dir.closeSync();
+ });
+}
+
+// fs.readdir
+{
+ assert.throws(() => {
+ fs.readdir(blockedFolder, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFolder),
+ }));
+
+ // doesNotThrow
+ fs.readdir(__dirname, (err) => {
+ assert.ifError(err);
+ });
+}
+
+// fs.watch
+{
+ assert.throws(() => {
+ fs.watch(blockedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.watch(relativeProtectedFile, () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+
+ // doesNotThrow
+ fs.readdir(__dirname, (err) => {
+ assert.ifError(err);
+ });
+}
+
+// fs.rename
+{
+ assert.throws(() => {
+ fs.rename(blockedFile, 'newfile', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.rename(relativeProtectedFile, 'newfile', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+}
+tmpdir.refresh();
diff --git a/test/parallel/test-permission-deny-fs-symlink-target-write.js b/test/parallel/test-permission-deny-fs-symlink-target-write.js
new file mode 100644
index 0000000000..4d508a2d52
--- /dev/null
+++ b/test/parallel/test-permission-deny-fs-symlink-target-write.js
@@ -0,0 +1,71 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+if (!common.canCreateSymLink())
+ common.skip('insufficient privileges');
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh(true);
+
+const readOnlyFolder = path.join(tmpdir.path, 'read-only');
+const readWriteFolder = path.join(tmpdir.path, 'read-write');
+const writeOnlyFolder = path.join(tmpdir.path, 'write-only');
+
+fs.mkdirSync(readOnlyFolder);
+fs.mkdirSync(readWriteFolder);
+fs.mkdirSync(writeOnlyFolder);
+fs.writeFileSync(path.join(readOnlyFolder, 'file'), 'evil file contents');
+fs.writeFileSync(path.join(readWriteFolder, 'file'), 'NO evil file contents');
+
+{
+ assert.ok(process.permission.deny('fs.write', [readOnlyFolder]));
+ assert.ok(process.permission.deny('fs.read', [writeOnlyFolder]));
+}
+
+{
+ // App won't be able to symlink from a readOnlyFolder
+ assert.throws(() => {
+ fs.symlink(path.join(readOnlyFolder, 'file'), path.join(readWriteFolder, 'link-to-read-only'), 'file', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(readOnlyFolder, 'file')),
+ }));
+
+ // App will be able to symlink to a writeOnlyFolder
+ fs.symlink(path.join(readWriteFolder, 'file'), path.join(writeOnlyFolder, 'link-to-read-write'), 'file', (err) => {
+ assert.ifError(err);
+ // App will won't be able to read the symlink
+ assert.throws(() => {
+ fs.readFile(path.join(writeOnlyFolder, 'link-to-read-write'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+
+ // App will be able to write to the symlink
+ fs.writeFile('file', 'some content', (err) => {
+ assert.ifError(err);
+ });
+ });
+
+ // App won't be able to symlink to a readOnlyFolder
+ assert.throws(() => {
+ fs.symlink(path.join(readWriteFolder, 'file'), path.join(readOnlyFolder, 'link-to-read-only'), 'file', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(readOnlyFolder, 'link-to-read-only')),
+ }));
+}
diff --git a/test/parallel/test-permission-deny-fs-symlink.js b/test/parallel/test-permission-deny-fs-symlink.js
new file mode 100644
index 0000000000..c093800519
--- /dev/null
+++ b/test/parallel/test-permission-deny-fs-symlink.js
@@ -0,0 +1,104 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const fixtures = require('../common/fixtures');
+if (!common.canCreateSymLink())
+ common.skip('insufficient privileges');
+
+const assert = require('assert');
+const fs = require('fs');
+
+const path = require('path');
+const tmpdir = require('../common/tmpdir');
+tmpdir.refresh(true);
+
+const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');
+const blockedFolder = path.join(tmpdir.path, 'subdirectory');
+const regularFile = __filename;
+const symlinkFromBlockedFile = path.join(tmpdir.path, 'example-symlink.md');
+
+fs.mkdirSync(blockedFolder);
+
+{
+ // Symlink previously created
+ fs.symlinkSync(blockedFile, symlinkFromBlockedFile);
+ assert.ok(process.permission.deny('fs.read', [blockedFile, blockedFolder]));
+ assert.ok(process.permission.deny('fs.write', [blockedFile, blockedFolder]));
+}
+
+{
+ // Previously created symlink are NOT affected by the permission model
+ const linkData = fs.readlinkSync(symlinkFromBlockedFile);
+ assert.ok(linkData);
+ const fileData = fs.readFileSync(symlinkFromBlockedFile);
+ assert.ok(fileData);
+ // cleanup
+ fs.unlink(symlinkFromBlockedFile, (err) => {
+ assert.ifError(
+ err,
+ `Error while removing the symlink: ${symlinkFromBlockedFile}.
+ You may need to remove it manually to re-run the tests`
+ );
+ });
+}
+
+{
+ // App doesn’t have access to the BLOCKFOLDER
+ assert.throws(() => {
+ fs.opendir(blockedFolder, (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.writeFile(blockedFolder + '/new-file', 'data', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ }));
+
+ // App doesn’t have access to the BLOCKEDFILE folder
+ assert.throws(() => {
+ fs.readFile(blockedFile, (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.appendFile(blockedFile, 'data', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ }));
+
+ // App won't be able to symlink REGULARFILE to BLOCKFOLDER/asdf
+ assert.throws(() => {
+ fs.symlink(regularFile, blockedFolder + '/asdf', 'file', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ }));
+
+ // App won't be able to symlink BLOCKEDFILE to REGULARDIR
+ assert.throws(() => {
+ fs.symlink(blockedFile, path.join(__dirname, '/asdf'), 'file', (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+}
+tmpdir.refresh(true);
diff --git a/test/parallel/test-permission-deny-fs-wildcard.js b/test/parallel/test-permission-deny-fs-wildcard.js
new file mode 100644
index 0000000000..2e278cb60b
--- /dev/null
+++ b/test/parallel/test-permission-deny-fs-wildcard.js
@@ -0,0 +1,128 @@
+// Flags: --experimental-permission --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const assert = require('assert');
+const fs = require('fs');
+
+if (common.isWindows) {
+ const denyList = [
+ 'C:\\tmp\\*',
+ 'C:\\example\\foo*',
+ 'C:\\example\\bar*',
+ 'C:\\folder\\*',
+ 'C:\\show',
+ 'C:\\slower',
+ 'C:\\slown',
+ 'C:\\home\\foo\\*',
+ ];
+ assert.ok(process.permission.deny('fs.read', denyList));
+ assert.ok(process.permission.has('fs.read', 'C:\\slow'));
+ assert.ok(process.permission.has('fs.read', 'C:\\slows'));
+ assert.ok(!process.permission.has('fs.read', 'C:\\slown'));
+ assert.ok(!process.permission.has('fs.read', 'C:\\home\\foo'));
+ assert.ok(!process.permission.has('fs.read', 'C:\\home\\foo\\'));
+ assert.ok(process.permission.has('fs.read', 'C:\\home\\fo'));
+} else {
+ const denyList = [
+ '/tmp/*',
+ '/example/foo*',
+ '/example/bar*',
+ '/folder/*',
+ '/show',
+ '/slower',
+ '/slown',
+ '/home/foo/*',
+ ];
+ assert.ok(process.permission.deny('fs.read', denyList));
+ assert.ok(process.permission.has('fs.read', '/slow'));
+ assert.ok(process.permission.has('fs.read', '/slows'));
+ assert.ok(!process.permission.has('fs.read', '/slown'));
+ assert.ok(!process.permission.has('fs.read', '/home/foo'));
+ assert.ok(!process.permission.has('fs.read', '/home/foo/'));
+ assert.ok(process.permission.has('fs.read', '/home/fo'));
+}
+
+{
+ assert.throws(() => {
+ fs.readFile('/tmp/foo/file', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ // doesNotThrow
+ fs.readFile('/test.txt', () => {});
+ fs.readFile('/tmpd', () => {});
+}
+
+{
+ assert.throws(() => {
+ fs.readFile('/example/foo/file', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/example/foo2/file', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/example/foo2', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+
+ // doesNotThrow
+ fs.readFile('/example/fo/foo2.js', () => {});
+ fs.readFile('/example/for', () => {});
+}
+
+{
+ assert.throws(() => {
+ fs.readFile('/example/bar/file', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/example/bar2/file', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/example/bar', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+
+ // doesNotThrow
+ fs.readFile('/example/ba/foo2.js', () => {});
+}
+
+{
+ assert.throws(() => {
+ fs.readFile('/folder/a/subfolder/b', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/folder/a/subfolder/b/c.txt', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.readFile('/folder/a/foo2.js', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+}
diff --git a/test/parallel/test-permission-deny-fs-write.js b/test/parallel/test-permission-deny-fs-write.js
new file mode 100644
index 0000000000..1f0d2997be
--- /dev/null
+++ b/test/parallel/test-permission-deny-fs-write.js
@@ -0,0 +1,240 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const fixtures = require('../common/fixtures');
+
+const blockedFolder = fixtures.path('permission', 'deny', 'protected-folder');
+const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');
+const relativeProtectedFile = './test/fixtures/permission/deny/protected-file.md';
+const relativeProtectedFolder = './test/fixtures/permission/deny/protected-folder';
+const absoluteProtectedFile = path.resolve(relativeProtectedFile);
+const absoluteProtectedFolder = path.resolve(relativeProtectedFolder);
+
+const regularFolder = fixtures.path('permission', 'deny');
+const regularFile = fixtures.path('permission', 'deny', 'regular-file.md');
+
+{
+ assert.ok(process.permission.deny('fs.write', [blockedFolder]));
+ assert.ok(process.permission.deny('fs.write', [blockedFile]));
+}
+
+// fs.writeFile
+{
+ assert.throws(() => {
+ fs.writeFile(blockedFile, 'example', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.writeFile(relativeProtectedFile, 'example', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+
+ assert.throws(() => {
+ fs.writeFile(path.join(blockedFolder, 'anyfile'), 'example', () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+}
+
+// fs.createWriteStream
+{
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createWriteStream(blockedFile);
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ })).then(common.mustCall());
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createWriteStream(relativeProtectedFile);
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ })).then(common.mustCall());
+
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createWriteStream(path.join(blockedFolder, 'example'));
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'example')),
+ })).then(common.mustCall());
+}
+
+// fs.utimes
+{
+ assert.throws(() => {
+ fs.utimes(blockedFile, new Date(), new Date(), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.utimes(relativeProtectedFile, new Date(), new Date(), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+
+ assert.throws(() => {
+ fs.utimes(path.join(blockedFolder, 'anyfile'), new Date(), new Date(), () => {});
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
+ }));
+}
+
+// fs.mkdir
+{
+ assert.throws(() => {
+ fs.mkdir(path.join(blockedFolder, 'any-folder'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-folder')),
+ }));
+ assert.throws(() => {
+ fs.mkdir(path.join(relativeProtectedFolder, 'any-folder'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(absoluteProtectedFolder, 'any-folder')),
+ }));
+}
+
+// fs.rename
+{
+ assert.throws(() => {
+ fs.rename(blockedFile, path.join(blockedFile, 'renamed'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+ assert.throws(() => {
+ fs.rename(relativeProtectedFile, path.join(relativeProtectedFile, 'renamed'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(absoluteProtectedFile),
+ }));
+ assert.throws(() => {
+ fs.rename(blockedFile, path.join(regularFolder, 'renamed'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+
+ assert.throws(() => {
+ fs.rename(regularFile, path.join(blockedFolder, 'renamed'), (err) => {
+ assert.ifError(err);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'renamed')),
+ }));
+}
+
+// fs.copyFile
+{
+ assert.throws(() => {
+ fs.copyFileSync(regularFile, path.join(blockedFolder, 'any-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-file')),
+ }));
+ assert.throws(() => {
+ fs.copyFileSync(regularFile, path.join(relativeProtectedFolder, 'any-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(absoluteProtectedFolder, 'any-file')),
+ }));
+}
+
+// fs.cp
+{
+ assert.throws(() => {
+ fs.cpSync(regularFile, path.join(blockedFolder, 'any-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-file')),
+ }));
+ assert.throws(() => {
+ fs.cpSync(regularFile, path.join(relativeProtectedFolder, 'any-file'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(absoluteProtectedFolder, 'any-file')),
+ }));
+}
+
+// fs.rm
+{
+ assert.throws(() => {
+ fs.rmSync(blockedFolder, { recursive: true });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFolder),
+ }));
+ assert.throws(() => {
+ fs.rmSync(relativeProtectedFolder, { recursive: true });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(absoluteProtectedFolder),
+ }));
+
+ // The user shouldn't be capable to rmdir of a non-protected folder
+ // but that contains a protected file.
+ // The regularFolder contains a protected file
+ assert.throws(() => {
+ fs.rmSync(regularFolder, { recursive: true });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(blockedFile),
+ }));
+}
diff --git a/test/parallel/test-permission-deny-worker-threads-cli.js b/test/parallel/test-permission-deny-worker-threads-cli.js
new file mode 100644
index 0000000000..e817a78772
--- /dev/null
+++ b/test/parallel/test-permission-deny-worker-threads-cli.js
@@ -0,0 +1,26 @@
+// Flags: --experimental-permission --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const assert = require('assert');
+const {
+ Worker,
+ isMainThread,
+} = require('worker_threads');
+
+// Guarantee the initial state
+{
+ assert.ok(!process.permission.has('worker'));
+}
+
+if (isMainThread) {
+ assert.throws(() => {
+ new Worker(__filename);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'WorkerThreads',
+ }));
+} else {
+ assert.fail('it should not be called');
+}
diff --git a/test/parallel/test-permission-deny-worker-threads.js b/test/parallel/test-permission-deny-worker-threads.js
new file mode 100644
index 0000000000..741b7d1a57
--- /dev/null
+++ b/test/parallel/test-permission-deny-worker-threads.js
@@ -0,0 +1,32 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-worker
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+
+const {
+ Worker,
+ isMainThread,
+} = require('worker_threads');
+const { once } = require('events');
+
+async function createWorker() {
+ // doesNotThrow
+ const worker = new Worker(__filename);
+ await once(worker, 'exit');
+ // When a permission is set by API, the process shouldn't be able
+ // to create worker threads
+ assert.ok(process.permission.deny('worker'));
+ assert.throws(() => {
+ new Worker(__filename);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'WorkerThreads',
+ }));
+}
+
+if (isMainThread) {
+ createWorker();
+} else {
+ process.exit(0);
+}
diff --git a/test/parallel/test-permission-deny.js b/test/parallel/test-permission-deny.js
new file mode 100644
index 0000000000..323be59c10
--- /dev/null
+++ b/test/parallel/test-permission-deny.js
@@ -0,0 +1,97 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const fs = require('fs');
+const fsPromises = require('node:fs/promises');
+const assert = require('assert');
+const path = require('path');
+const fixtures = require('../common/fixtures');
+
+const protectedFolder = fixtures.path('permission', 'deny');
+const protectedFile = fixtures.path('permission', 'deny', 'protected-file.md');
+const regularFile = fixtures.path('permission', 'deny', 'regular-file.md');
+
+// Assert has and deny exists
+{
+ assert.ok(typeof process.permission.has === 'function');
+ assert.ok(typeof process.permission.deny === 'function');
+}
+
+// Guarantee the initial state when no flags
+{
+ assert.ok(process.permission.has('fs.read'));
+ assert.ok(process.permission.has('fs.write'));
+
+ assert.ok(process.permission.has('fs.read', protectedFile));
+ assert.ok(process.permission.has('fs.read', regularFile));
+
+ assert.ok(process.permission.has('fs.write', protectedFolder));
+ assert.ok(process.permission.has('fs.write', regularFile));
+
+ // doesNotThrow
+ fs.readFileSync(protectedFile);
+}
+
+// Deny access to fs.read
+{
+ assert.ok(process.permission.deny('fs.read', [protectedFile]));
+ assert.ok(process.permission.has('fs.read'));
+ assert.ok(process.permission.has('fs.write'));
+
+ assert.ok(process.permission.has('fs.read', regularFile));
+ assert.ok(!process.permission.has('fs.read', protectedFile));
+
+ assert.ok(process.permission.has('fs.write', protectedFolder));
+ assert.ok(process.permission.has('fs.write', regularFile));
+
+ assert.rejects(() => {
+ return fsPromises.readFile(protectedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ })).then(common.mustCall());
+
+ // doesNotThrow
+ fs.openSync(regularFile, 'w');
+}
+
+// Deny access to fs.write
+{
+ assert.ok(process.permission.deny('fs.write', [protectedFolder]));
+ assert.ok(process.permission.has('fs.read'));
+ assert.ok(process.permission.has('fs.write'));
+
+ assert.ok(!process.permission.has('fs.read', protectedFile));
+ assert.ok(process.permission.has('fs.read', regularFile));
+
+ assert.ok(!process.permission.has('fs.write', protectedFolder));
+ assert.ok(!process.permission.has('fs.write', regularFile));
+
+ assert.rejects(() => {
+ return fsPromises
+ .writeFile(path.join(protectedFolder, 'new-file'), 'data');
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ })).then(common.mustCall());
+
+ assert.throws(() => {
+ fs.openSync(regularFile, 'w');
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ }));
+}
+
+// Should not crash if wrong parameter is provided
+{
+ // Array is expected as second parameter
+ assert.throws(() => {
+ process.permission.deny('fs.read', protectedFolder);
+ }, common.expectsError({
+ code: 'ERR_INVALID_ARG_TYPE',
+ }));
+}
diff --git a/test/parallel/test-permission-experimental.js b/test/parallel/test-permission-experimental.js
new file mode 100644
index 0000000000..bec66e5a73
--- /dev/null
+++ b/test/parallel/test-permission-experimental.js
@@ -0,0 +1,13 @@
+// Flags: --experimental-permission --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+const assert = require('assert');
+
+// This test ensures that the experimental message is emitted
+// when using permission system
+
+process.on('warning', common.mustCall((warning) => {
+ assert.match(warning.message, /Permission is an experimental feature/);
+}, 1));
diff --git a/test/parallel/test-permission-fs-relative-path.js b/test/parallel/test-permission-fs-relative-path.js
new file mode 100644
index 0000000000..73f0635d98
--- /dev/null
+++ b/test/parallel/test-permission-fs-relative-path.js
@@ -0,0 +1,48 @@
+// Flags: --experimental-permission --allow-fs-read=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const assert = require('assert');
+const fixtures = require('../common/fixtures');
+const { spawnSync } = require('child_process');
+
+const protectedFile = fixtures.path('permission', 'deny', 'protected-file.md');
+const relativeProtectedFile = './test/fixtures/permission/deny/protected-file.md';
+
+// Note: for relative path on fs.* calls, check test-permission-deny-fs-[read/write].js files
+
+{
+ // permission.deny relative path should work
+ assert.ok(process.permission.has('fs.read', protectedFile));
+ assert.ok(process.permission.deny('fs.read', [relativeProtectedFile]));
+ assert.ok(!process.permission.has('fs.read', protectedFile));
+}
+
+{
+ // permission.has relative path should work
+ assert.ok(!process.permission.has('fs.read', relativeProtectedFile));
+}
+
+{
+ // Relative path as CLI args are NOT supported yet
+ const { status, stdout } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission',
+ '--allow-fs-read', '*',
+ '--allow-fs-write', '../fixtures/permission/deny/regular-file.md',
+ '-e',
+ `
+ const path = require("path");
+ const absolutePath = path.resolve("../fixtures/permission/deny/regular-file.md");
+ console.log(process.permission.has("fs.write", absolutePath));
+ `,
+ ]
+ );
+
+ const [fsWrite] = stdout.toString().split('\n');
+ assert.strictEqual(fsWrite, 'false');
+ assert.strictEqual(status, 0);
+}
diff --git a/test/parallel/test-permission-fs-windows-path.js b/test/parallel/test-permission-fs-windows-path.js
new file mode 100644
index 0000000000..90d377f0c7
--- /dev/null
+++ b/test/parallel/test-permission-fs-windows-path.js
@@ -0,0 +1,66 @@
+// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+'use strict';
+
+const common = require('../common');
+common.skipIfWorker();
+
+const assert = require('assert');
+const fixtures = require('../common/fixtures');
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+if (!common.isWindows) {
+ common.skip('windows test');
+}
+
+const protectedFolder = fixtures.path('permission', 'deny', 'protected-folder');
+
+{
+ assert.ok(process.permission.has('fs.write', protectedFolder));
+ assert.ok(process.permission.deny('fs.write', [protectedFolder]));
+ assert.ok(!process.permission.has('fs.write', protectedFolder));
+}
+
+{
+ assert.throws(() => {
+ fs.openSync(path.join(protectedFolder, 'protected-file.md'), 'w');
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(protectedFolder, 'protected-file.md')),
+ }));
+
+ assert.rejects(() => {
+ return new Promise((_resolve, reject) => {
+ const stream = fs.createWriteStream(path.join(protectedFolder, 'protected-file.md'));
+ stream.on('error', reject);
+ });
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ resource: path.toNamespacedPath(path.join(protectedFolder, 'protected-file.md')),
+ })).then(common.mustCall());
+}
+
+{
+ const { stdout } = spawnSync(process.execPath, [
+ '--experimental-permission', '--allow-fs-write', 'C:\\\\', '-e',
+ 'console.log(process.permission.has("fs.write", "C:\\\\"))',
+ ]);
+ assert.strictEqual(stdout.toString(), 'true\n');
+}
+
+{
+ assert.ok(process.permission.has('fs.write', 'C:\\home'));
+ assert.ok(process.permission.deny('fs.write', ['C:\\home']));
+ assert.ok(!process.permission.has('fs.write', 'C:\\home'));
+}
+
+{
+ assert.ok(process.permission.has('fs.write', '\\\\?\\C:\\'));
+ assert.ok(process.permission.deny('fs.write', ['\\\\?\\C:\\']));
+ // UNC aren't supported so far
+ assert.ok(process.permission.has('fs.write', 'C:/'));
+ assert.ok(process.permission.has('fs.write', '\\\\?\\C:\\'));
+}
diff --git a/test/parallel/test-permission-warning-flags.js b/test/parallel/test-permission-warning-flags.js
new file mode 100644
index 0000000000..f62b39fbe5
--- /dev/null
+++ b/test/parallel/test-permission-warning-flags.js
@@ -0,0 +1,23 @@
+'use strict';
+
+require('../common');
+const { spawnSync } = require('child_process');
+const assert = require('assert');
+
+const warnFlags = [
+ '--allow-child-process',
+ '--allow-worker',
+];
+
+for (const flag of warnFlags) {
+ const { status, stderr } = spawnSync(
+ process.execPath,
+ [
+ '--experimental-permission', flag, '-e',
+ 'setTimeout(() => {}, 1)',
+ ]
+ );
+
+ assert.match(stderr.toString(), new RegExp(`SecurityWarning: The flag ${flag} must be used with extreme caution`));
+ assert.strictEqual(status, 0);
+}
diff --git a/tools/run-worker.js b/tools/run-worker.js
index 7590e460a4..20f03f53e1 100644
--- a/tools/run-worker.js
+++ b/tools/run-worker.js
@@ -7,5 +7,13 @@ if (typeof require === 'undefined') {
const path = require('path');
const { Worker } = require('worker_threads');
+// When --experimental-permission is enabled, the process
+// aren't able to spawn any worker unless --allow-worker is passed.
+// Therefore, we skip the permission tests for custom-suites-freestyle
+if (process.permission && !process.permission.has('worker')) {
+ console.log('1..0 # Skipped: Not being run with worker_threads permission');
+ process.exit(0);
+}
+
new Worker(path.resolve(process.cwd(), process.argv[2]))
.on('exit', (code) => process.exitCode = code);