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:
Rafael Gonzaga 2023-02-23 15:11:51 -03:00 committed by GitHub
parent 42be7f6a03
commit 00c222593e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 3246 additions and 19 deletions

View 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();
}

View 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);
}

View 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);
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.
.

View File

@ -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);

View File

@ -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,

View File

@ -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);

View 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);
},
});

View File

@ -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) {

View File

@ -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;

View File

@ -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',

View File

@ -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_;
}

View File

@ -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,

View File

@ -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;

View File

@ -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") \

View File

@ -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())

View File

@ -58,6 +58,7 @@
V(options) \
V(os) \
V(performance) \
V(permission) \
V(pipe_wrap) \
V(process_wrap) \
V(process_methods) \

View File

@ -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);

View File

@ -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") \

View File

@ -78,6 +78,7 @@ class ExternalReferenceRegistry {
V(options) \
V(os) \
V(performance) \
V(permission) \
V(process_methods) \
V(process_object) \
V(report) \

View File

@ -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();

View File

@ -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,

View File

@ -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;

View File

@ -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());

View 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

View 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_

View 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

View 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_

View 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)

View 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_

View 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_

View 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

View 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_

View File

@ -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();

View File

@ -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) \

View 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);
}

View File

@ -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

View File

@ -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);

View File

@ -0,0 +1,3 @@
# Protected File
Example of a protected file to be used in the PolicyDenyFs module

View File

@ -0,0 +1,3 @@
# Protected File
Example of a protected file to be used in the PolicyDenyFs module

View File

View 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',

View File

@ -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' });

View 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'));
}

View File

@ -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']);
}

View 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));
}

View 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',
}));
}

View 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',
}));
}

View 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();

View File

@ -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')),
}));
}

View 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 doesnt 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 doesnt 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);

View 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',
}));
}

View 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),
}));
}

View 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');
}

View 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);
}

View 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',
}));
}

View 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));

View 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);
}

View 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:\\'));
}

View 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);
}

View File

@ -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);