mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
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>
This commit is contained in:
parent
42be7f6a03
commit
00c222593e
72
benchmark/fs/readfile-permission-enabled.js
Normal file
72
benchmark/fs/readfile-permission-enabled.js
Normal file
@ -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();
|
||||
}
|
19
benchmark/permission/permission-fs-deny.js
Normal file
19
benchmark/permission/permission-fs-deny.js
Normal file
@ -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);
|
||||
}
|
50
benchmark/permission/permission-fs-is-granted.js
Normal file
50
benchmark/permission/permission-fs-is-granted.js
Normal file
@ -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);
|
||||
}
|
169
doc/api/cli.md
169
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
15
doc/node.1
15
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.
|
||||
.
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
56
lib/internal/process/permission.js
Normal file
56
lib/internal/process/permission.js
Normal file
@ -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);
|
||||
},
|
||||
});
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
9
node.gyp
9
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',
|
||||
|
@ -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_;
|
||||
}
|
||||
|
25
src/env.cc
25
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,
|
||||
|
@ -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;
|
||||
|
@ -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") \
|
||||
|
@ -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())
|
||||
|
@ -58,6 +58,7 @@
|
||||
V(options) \
|
||||
V(os) \
|
||||
V(performance) \
|
||||
V(permission) \
|
||||
V(pipe_wrap) \
|
||||
V(process_wrap) \
|
||||
V(process_methods) \
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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") \
|
||||
|
@ -78,6 +78,7 @@ class ExternalReferenceRegistry {
|
||||
V(options) \
|
||||
V(os) \
|
||||
V(performance) \
|
||||
V(permission) \
|
||||
V(process_methods) \
|
||||
V(process_object) \
|
||||
V(report) \
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
27
src/permission/child_process_permission.cc
Normal file
27
src/permission/child_process_permission.cc
Normal file
@ -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
|
30
src/permission/child_process_permission.h
Normal file
30
src/permission/child_process_permission.h
Normal file
@ -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_
|
216
src/permission/fs_permission.cc
Normal file
216
src/permission/fs_permission.cc
Normal file
@ -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
|
163
src/permission/fs_permission.h
Normal file
163
src/permission/fs_permission.h
Normal file
@ -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_
|
200
src/permission/permission.cc
Normal file
200
src/permission/permission.cc
Normal file
@ -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)
|
73
src/permission/permission.h
Normal file
73
src/permission/permission.h
Normal file
@ -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_
|
51
src/permission/permission_base.h
Normal file
51
src/permission/permission_base.h
Normal file
@ -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_
|
26
src/permission/worker_permission.cc
Normal file
26
src/permission/worker_permission.cc
Normal file
@ -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
|
30
src/permission/worker_permission.h
Normal file
30
src/permission/worker_permission.h
Normal file
@ -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_
|
@ -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();
|
||||
|
@ -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) \
|
||||
|
43
test/addons/no-addons/permission.js
Normal file
43
test/addons/no-addons/permission.js
Normal file
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
3
test/fixtures/permission/deny/protected-file.md
vendored
Normal file
3
test/fixtures/permission/deny/protected-file.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Protected File
|
||||
|
||||
Example of a protected file to be used in the PolicyDenyFs module
|
3
test/fixtures/permission/deny/protected-folder/protected-file.md
vendored
Normal file
3
test/fixtures/permission/deny/protected-folder/protected-file.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Protected File
|
||||
|
||||
Example of a protected file to be used in the PolicyDenyFs module
|
0
test/fixtures/permission/deny/regular-file.md
vendored
Normal file
0
test/fixtures/permission/deny/regular-file.md
vendored
Normal file
@ -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',
|
||||
|
@ -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' });
|
||||
|
||||
|
128
test/parallel/test-cli-permission-deny-fs.js
Normal file
128
test/parallel/test-cli-permission-deny-fs.js
Normal file
@ -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'));
|
||||
}
|
@ -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']);
|
||||
}
|
22
test/parallel/test-permission-deny-allow-worker-cli.js
Normal file
22
test/parallel/test-permission-deny-allow-worker-cli.js
Normal file
@ -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));
|
||||
}
|
45
test/parallel/test-permission-deny-child-process-cli.js
Normal file
45
test/parallel/test-permission-deny-child-process-cli.js
Normal file
@ -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',
|
||||
}));
|
||||
}
|
52
test/parallel/test-permission-deny-child-process.js
Normal file
52
test/parallel/test-permission-deny-child-process.js
Normal file
@ -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',
|
||||
}));
|
||||
}
|
328
test/parallel/test-permission-deny-fs-read.js
Normal file
328
test/parallel/test-permission-deny-fs-read.js
Normal file
@ -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();
|
@ -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')),
|
||||
}));
|
||||
}
|
104
test/parallel/test-permission-deny-fs-symlink.js
Normal file
104
test/parallel/test-permission-deny-fs-symlink.js
Normal file
@ -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);
|
128
test/parallel/test-permission-deny-fs-wildcard.js
Normal file
128
test/parallel/test-permission-deny-fs-wildcard.js
Normal file
@ -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',
|
||||
}));
|
||||
}
|
240
test/parallel/test-permission-deny-fs-write.js
Normal file
240
test/parallel/test-permission-deny-fs-write.js
Normal file
@ -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),
|
||||
}));
|
||||
}
|
26
test/parallel/test-permission-deny-worker-threads-cli.js
Normal file
26
test/parallel/test-permission-deny-worker-threads-cli.js
Normal file
@ -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');
|
||||
}
|
32
test/parallel/test-permission-deny-worker-threads.js
Normal file
32
test/parallel/test-permission-deny-worker-threads.js
Normal file
@ -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);
|
||||
}
|
97
test/parallel/test-permission-deny.js
Normal file
97
test/parallel/test-permission-deny.js
Normal file
@ -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',
|
||||
}));
|
||||
}
|
13
test/parallel/test-permission-experimental.js
Normal file
13
test/parallel/test-permission-experimental.js
Normal file
@ -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));
|
48
test/parallel/test-permission-fs-relative-path.js
Normal file
48
test/parallel/test-permission-fs-relative-path.js
Normal file
@ -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);
|
||||
}
|
66
test/parallel/test-permission-fs-windows-path.js
Normal file
66
test/parallel/test-permission-fs-windows-path.js
Normal file
@ -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:\\'));
|
||||
}
|
23
test/parallel/test-permission-warning-flags.js
Normal file
23
test/parallel/test-permission-warning-flags.js
Normal file
@ -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);
|
||||
}
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user