Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src,permission: disable WASI when pm is enabled #53124

Merged
merged 2 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -927,6 +974,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 @@ -2643,6 +2691,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 @@ -3184,6 +3233,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 @@ -595,6 +596,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 @@ -529,6 +529,7 @@ function initializePermission() {
const warnFlags = [
'--allow-addons',
'--allow-child-process',
'--allow-wasi',
'--allow-worker',
];
for (const flag of warnFlags) {
Expand Down Expand Up @@ -570,6 +571,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 @@ -456,6 +456,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 @@ -130,6 +130,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',
}));
}