Skip to content

Commit

Permalink
src,permission: --allow-wasi & prevent WASI exec
Browse files Browse the repository at this point in the history
PR-URL: #53124
Reviewed-By: Marco Ippolito <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
  • Loading branch information
RafaelGSS authored and targos committed Jun 3, 2024
1 parent 6708536 commit 75e5612
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 7 deletions.
50 changes: 50 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,53 @@ Examples can be found in the [File System Permissions][] documentation.

Relative paths are NOT supported through the CLI flag.

### `--allow-wasi`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development
When using the [Permission Model][], the process will not be capable of creating
any WASI instances by default.
For security reasons, the call will throw an `ERR_ACCESS_DENIED` unless the
user explicitly passes the flag `--allow-wasi` in the main Node.js process.

Example:

```js
const { WASI } = require('node:wasi');
// Attempt to bypass the permission
new WASI({
version: 'preview1',
// Attempt to mount the whole filesystem
preopens: {
'/': '/',
},
});
```

```console
$ node --experimental-permission --allow-fs-read=* index.js
node:wasi:99
const wrap = new _WASI(args, env, preopens, stdio);
^

Error: Access to this API has been restricted
at new WASI (node:wasi:99:18)
at Object.<anonymous> (/home/index.js:3:1)
at Module._compile (node:internal/modules/cjs/loader:1476:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1555:10)
at Module.load (node:internal/modules/cjs/loader:1288:32)
at Module._load (node:internal/modules/cjs/loader:1104:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:191:14)
at node:internal/main/run_main_module:30:49 {
code: 'ERR_ACCESS_DENIED',
permission: 'WASI',
}
```

### `--allow-worker`

<!-- YAML
Expand Down Expand Up @@ -925,6 +972,7 @@ following permissions are restricted:
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag

### `--experimental-require-module`

Expand Down Expand Up @@ -2738,6 +2786,7 @@ one is included in the list below.
* `--allow-child-process`
* `--allow-fs-read`
* `--allow-fs-write`
* `--allow-wasi`
* `--allow-worker`
* `--conditions`, `-C`
* `--diagnostic-dir`
Expand Down Expand Up @@ -3293,6 +3342,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--allow-child-process`]: #--allow-child-process
[`--allow-fs-read`]: #--allow-fs-read
[`--allow-fs-write`]: #--allow-fs-write
[`--allow-wasi`]: #--allow-wasi
[`--allow-worker`]: #--allow-worker
[`--build-snapshot`]: #--build-snapshot
[`--cpu-prof-dir`]: #--cpu-prof-dir
Expand Down
6 changes: 4 additions & 2 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ flag.

When starting Node.js with `--experimental-permission`,
the ability to access the file system through the `fs` module, spawn processes,
use `node:worker_threads`, native addons, and enable the runtime inspector
use `node:worker_threads`, use native addons, use WASI, and enable the runtime inspector
will be restricted.

```console
Expand All @@ -507,7 +507,7 @@ Allowing access to spawning a process and creating worker threads can be done
using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.

To allow native addons when using permission model, use the [`--allow-addons`][]
flag.
flag. For WASI, use the [`--allow-wasi`][] flag.

#### Runtime API

Expand Down Expand Up @@ -574,6 +574,7 @@ There are constraints you need to know before using this system:
* Worker Threads
* Inspector protocol
* File system access
* WASI
* The Permission Model is initialized after the Node.js environment is set up.
However, certain flags such as `--env-file` or `--openssl-config` are designed
to read files before environment initialization. As a result, such flags are
Expand All @@ -597,6 +598,7 @@ There are constraints you need to know before using this system:
[`--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-wasi`]: cli.md#--allow-wasi
[`--allow-worker`]: cli.md#--allow-worker
[`--experimental-permission`]: cli.md#--experimental-permission
[`permission.has()`]: process.md#processpermissionhasscope-reference
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ Allow using native addons when using the permission model.
.It Fl -allow-child-process
Allow spawning process when using the permission model.
.
.It Fl -allow-wasi
Allow execution of WASI when using the permission model.
.
.It Fl -allow-worker
Allow creating worker threads when using the permission model.
.
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ function initializePermission() {
const warnFlags = [
'--allow-addons',
'--allow-child-process',
'--allow-wasi',
'--allow-worker',
];
for (const flag of warnFlags) {
Expand Down Expand Up @@ -631,6 +632,7 @@ function initializePermission() {
'--allow-fs-write',
'--allow-addons',
'--allow-child-process',
'--allow-wasi',
'--allow-worker',
];
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
'src/permission/fs_permission.cc',
'src/permission/inspector_permission.cc',
'src/permission/permission.cc',
'src/permission/wasi_permission.cc',
'src/permission/worker_permission.cc',
'src/pipe_wrap.cc',
'src/process_wrap.cc',
Expand Down Expand Up @@ -277,6 +278,7 @@
'src/permission/fs_permission.h',
'src/permission/inspector_permission.h',
'src/permission/permission.h',
'src/permission/wasi_permission.h',
'src/permission/worker_permission.h',
'src/pipe_wrap.h',
'src/req_wrap.h',
Expand Down
3 changes: 3 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,9 @@ Environment::Environment(IsolateData* isolate_data,
permission()->Apply(
this, {"*"}, permission::PermissionScope::kWorkerThreads);
}
if (!options_->allow_wasi) {
permission()->Apply(this, {"*"}, permission::PermissionScope::kWASI);
}

if (!options_->allow_fs_read.empty()) {
permission()->Apply(this,
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"allow use of child process when any permissions are set",
&EnvironmentOptions::allow_child_process,
kAllowedInEnvvar);
AddOption("--allow-wasi",
"allow wasi when any permissions are set",
&EnvironmentOptions::allow_wasi,
kAllowedInEnvvar);
AddOption("--allow-worker",
"allow worker threads when any permissions are set",
&EnvironmentOptions::allow_worker_threads,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> allow_fs_write;
bool allow_addons = false;
bool allow_child_process = false;
bool allow_wasi = false;
bool allow_worker_threads = false;
bool experimental_repl_await = true;
bool experimental_vm_modules = false;
Expand Down
12 changes: 7 additions & 5 deletions src/node_wasi.cc
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#include "env-inl.h"
#include "node_wasi.h"
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node_mem-inl.h"
#include "util-inl.h"
#include "node.h"
#include "node_errors.h"
#include "node_mem-inl.h"
#include "permission/permission.h"
#include "util-inl.h"
#include "uv.h"
#include "uvwasi.h"
#include "node_wasi.h"

namespace node {
namespace wasi {
Expand Down Expand Up @@ -120,8 +121,9 @@ void WASI::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args[1]->IsArray());
CHECK(args[2]->IsArray());
CHECK(args[3]->IsArray());

Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kWASI, "");
Local<Context> context = env->context();
Local<Array> argv = args[0].As<Array>();
const uint32_t argc = argv->Length();
Expand Down
5 changes: 5 additions & 0 deletions src/permission/permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Permission::Permission() : enabled_(false) {
std::make_shared<WorkerPermission>();
std::shared_ptr<PermissionBase> inspector =
std::make_shared<InspectorPermission>();
std::shared_ptr<PermissionBase> wasi = std::make_shared<WASIPermission>();
#define V(Name, _, __) \
nodes_.insert(std::make_pair(PermissionScope::k##Name, fs));
FILESYSTEM_PERMISSIONS(V)
Expand All @@ -98,6 +99,10 @@ Permission::Permission() : enabled_(false) {
nodes_.insert(std::make_pair(PermissionScope::k##Name, inspector));
INSPECTOR_PERMISSIONS(V)
#undef V
#define V(Name, _, __) \
nodes_.insert(std::make_pair(PermissionScope::k##Name, wasi));
WASI_PERMISSIONS(V)
#undef V
}

Local<Value> CreateAccessDeniedError(Environment* env,
Expand Down
1 change: 1 addition & 0 deletions src/permission/permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "permission/fs_permission.h"
#include "permission/inspector_permission.h"
#include "permission/permission_base.h"
#include "permission/wasi_permission.h"
#include "permission/worker_permission.h"
#include "v8.h"

Expand Down
3 changes: 3 additions & 0 deletions src/permission/permission_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace permission {

#define CHILD_PROCESS_PERMISSIONS(V) V(ChildProcess, "child", PermissionsRoot)

#define WASI_PERMISSIONS(V) V(WASI, "wasi", PermissionsRoot)

#define WORKER_THREADS_PERMISSIONS(V) \
V(WorkerThreads, "worker", PermissionsRoot)

Expand All @@ -29,6 +31,7 @@ namespace permission {
#define PERMISSIONS(V) \
FILESYSTEM_PERMISSIONS(V) \
CHILD_PROCESS_PERMISSIONS(V) \
WASI_PERMISSIONS(V) \
WORKER_THREADS_PERMISSIONS(V) \
INSPECTOR_PERMISSIONS(V)

Expand Down
25 changes: 25 additions & 0 deletions src/permission/wasi_permission.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "permission/wasi_permission.h"

#include <string>
#include <vector>

namespace node {

namespace permission {

// Currently, WASIPermission manage a single state
// Once denied, it's always denied
void WASIPermission::Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) {
deny_all_ = true;
}

bool WASIPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
return deny_all_ == false;
}

} // namespace permission
} // namespace node
31 changes: 31 additions & 0 deletions src/permission/wasi_permission.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef SRC_PERMISSION_WASI_PERMISSION_H_
#define SRC_PERMISSION_WASI_PERMISSION_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <vector>
#include "permission/permission_base.h"

namespace node {

namespace permission {

class WASIPermission final : public PermissionBase {
public:
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;

private:
bool deny_all_;
};

} // namespace permission

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_PERMISSION_WASI_PERMISSION_H_
22 changes: 22 additions & 0 deletions test/parallel/test-permission-allow-wasi-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Flags: --experimental-permission --allow-wasi --allow-fs-read=*
'use strict';

const common = require('../common');
common.skipIfWorker();

const assert = require('assert');
const { WASI } = require('wasi');

// Guarantee the initial state
{
assert.ok(process.permission.has('wasi'));
}

// When a permission is set by cli, the process shouldn't be able
// to create WASI instance unless --allow-wasi is sent
{
// doesNotThrow
new WASI({
version: 'preview1',
});
}
1 change: 1 addition & 0 deletions test/parallel/test-permission-warning-flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const assert = require('assert');
const warnFlags = [
'--allow-addons',
'--allow-child-process',
'--allow-wasi',
'--allow-worker',
];

Expand Down
19 changes: 19 additions & 0 deletions test/parallel/test-permission-wasi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Flags: --experimental-permission --allow-fs-read=*
'use strict';

const common = require('../common');
const assert = require('node:assert');

const { WASI } = require('wasi');

{
assert.throws(() => {
new WASI({
version: 'preview1',
preopens: { '/': '/' },
});
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'WASI',
}));
}

0 comments on commit 75e5612

Please sign in to comment.