8000 src,permission: add --allow-net permission by RafaelGSS · Pull Request #58517 · nodejs/node · GitHub
[go: up one dir, main page]

Skip to content
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
35 changes: 35 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,38 @@ When passing a single flag with a comma a warning will be displayed.

Examples can be found in the [File System Permissions][] documentation.

### `--allow-net`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development

When using the [Permission Model][], the process will not be able to access
network by default.
Attempts to do so will throw an `ERR_ACCESS_DENIED` unless the
user explicitly passes the `--allow-net` flag when starting Node.js.

Example:

```js
const http = require('node:http');
// Attempt to bypass the permission
const req = http.get('http://example.com', () => {});

req.on('error', (err) => {
console.log('err', err);
});
```

```console
$ node --permission index.js
Error: connect ERR_ACCESS_DENIED Access to this API has been restricted. Use --allow-net to manage permissions.
code: 'ERR_ACCESS_DENIED',
}
```

### `--allow-wasi`

<!-- YAML
Expand Down Expand Up @@ -1932,6 +1964,7 @@ following permissions are restricted:

* File System - manageable through
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
* Network - manageable through [`--allow-net`][] flag
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
Expand Down Expand Up @@ -3278,6 +3311,7 @@ one is included in the list below.
* `--allow-child-process`
* `--allow-fs-read`
* `--allow-fs-write`
* `--allow-net`
* `--allow-wasi`
* `--allow-worker`
* `--conditions`, `-C`
Expand Down Expand Up @@ -3880,6 +3914,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-net`]: #--allow-net
[`--allow-wasi`]: #--allow-wasi
[`--allow-worker`]: #--allow-worker
[`--build-snapshot`]: #--build-snapshot
Expand Down
11 changes: 7 additions & 4 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ The available permissions are documented by the [`--permission`][]
flag.

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

```console
$ node --permission index.js
Expand All @@ -69,7 +69,8 @@ Error: Access to this API has been restricted
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`][]
To allow network access, use [`--allow-net`][] and for allowing native addons
when using permission model, use the [`--allow-addons`][]
flag. For WASI, use the [`--allow-wasi`][] flag.

#### Runtime API
Expand Down Expand Up @@ -180,6 +181,7 @@ There are constraints you need to know before using this system:
* The model does not inherit to a child node process or a worker thread.
* When using the Permission Model the following features will be restricted:
* Native modules
* Network
* Child process
* Worker Threads
* Inspector protocol
Expand Down Expand Up @@ -210,6 +212,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-net`]: cli.md#--allow-net
[`--allow-wasi`]: cli.md#--allow-wasi
[`--allow-worker`]: cli.md#--allow-worker
[`--permission`]: cli.md#--permission
Expand Down
3 changes: 3 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
}
]
},
"allow-net": {
"type": "boolean"
},
"allow-wasi": {
"type": "boolean"
},
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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-net
Allow network access when using the permission model.
.
.It Fl -allow-wasi
Allow execution of WASI when using the permission model.
.
Expand Down
33 changes: 23 additions & 10 deletions lib/internal/errors.js
9E88
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ function setInternalPrepareStackTrace(callback) {
internalPrepareStackTrace = callback;
}

function isPermissionModelError(err) {
return typeof err !== 'number' && err.code && err.code === 'ERR_ACCESS_DENIED';
}

/**
* Every realm has its own prepareStackTraceCallback. When `error.stack` is
* accessed, if the error is created in a shadow realm, the shadow realm's
Expand Down Expand Up @@ -762,17 +766,23 @@ class ExceptionWithHostPort extends Error {
// This can be replaced with [ code ] = errmap.get(err) when this method
// is no longer exposed to user land.
util ??= require('util');
const code = util.getSystemErrorName(err);
let code;
let details = '';
if (port && port > 0) {
details = ` ${address}:${port}`;
} else if (address) {
details = ` ${address}`;
}
if (additional) {
details += ` - Local (${additional})`;
// True when permission model is enabled
if (isPermissionModelError(err)) {
code = err.code;
details = ` ${err.message}`;
} else {
code = util.getSystemErrorName(err);
if (port && port > 0) {
details = ` ${address}:${port}`;
} else if (address) {
details = ` ${address}`;
}
if (additional) {
details += ` - Local (${additional})`;
}
}

super(`${syscall} ${code}${details}`);

this.errno = err;
Expand All @@ -798,7 +808,7 @@ class DNSException extends Error {
constructor(code, syscall, hostname) {
let errno;
// If `code` is of type number, it is a libuv error number, else it is a
// c-ares error code.
// c-ares/permission model error code.
// TODO(joyeecheung): translate c-ares error codes into numeric ones and
// make them available in a property that's not error.errno (since they
// can be in conflict with libuv error codes). Also make sure
Expand All @@ -813,6 +823,9 @@ class DNSException extends Error {
} else {
code = lazyInternalUtil().getSystemErrorName(code);
}
} else if (isPermissionModelError(code)) {
// Expects a ERR_ACCESS_DENIED object
code = code.code;
}
super(`${syscall} ${code}${hostname ? ` ${hostname}` : ''}`);
this.errno = errno;
Expand Down
12 changes: 12 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,17 @@ function initializePermission() {
}
}

const experimentalWarnFlags = [
'--allow-net',
];
for (const flag of experimentalWarnFlags) {
if (getOptionValue(flag)) {
process.emitWarning(
`The flag ${flag} is under experimental phase.`,
'ExperimentalWarning');
}
}

ObjectDefineProperty(process, 'permission', {
__proto__: null,
enumerable: true,
Expand All @@ -614,6 +625,7 @@ function initializePermission() {
'--allow-fs-write',
'--allow-addons',
'--allow-child-process',
'--allow-net',
'--allow-wasi',
'--allow-worker',
];
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
'src/permission/permission.cc',
'src/permission/wasi_permission.cc',
'src/permission/worker_permission.cc',
'src/permission/net_permission.cc',
'src/pipe_wrap.cc',
'src/process_wrap.cc',
'src/signal_wrap.cc',
Expand Down Expand Up @@ -291,6 +292,7 @@
'src/permission/permission.h',
'src/permission/wasi_permission.h',
'src/permission/worker_permission.h',
'src/permission/net_permission.h',
'src/pipe_wrap.h',
'src/req_wrap.h',
'src/req_wrap-inl.h',
Expand Down
30 changes: 26 additions & 4 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,16 @@ Maybe<int> SoaTraits::Parse(QuerySoaWrap* wrap,
}

int ReverseTraits::Send(QueryReverseWrap* wrap, const char* name) {
permission::PermissionScope scope = permission::PermissionScope::kNet;
Environment* env_holder = wrap->env();

if (!env_holder->permission()->is_granted(env_holder, scope, name))
[[unlikely]] {
wrap->QueuePermissionModelResponseCallback(name);
// Error will be returned in the callback
return ARES_SUCCESS;
}

int length, family;
char address_buffer[sizeof(struct in6_addr)];

Expand Down Expand Up @@ -1851,6 +1861,10 @@ void GetAddrInfo(const FunctionCallbackInfo<Value>& args) {
CHECK(args[4]->IsUint32());
Local<Object> req_wrap_obj = args[0].As<Object>();
node::Utf8Value hostname(env->isolate(), args[1]);

ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kNet, hostname.ToStringView(), args);

std::string ascii_hostname = ada::idna::to_ascii(hostname.ToStringView());

int32_t flags = 0;
Expand Down Expand Up @@ -1925,10 +1939,18 @@ void GetNameInfo(const FunctionCallbackInfo<Value>& args) {
TRACING_CATEGORY_NODE2(dns, native), "lookupService", req_wrap.get(),
"ip", TRACE_STR_COPY(*ip), "port", port);

int err = req_wrap->Dispatch(uv_getnameinfo,
AfterGetNameInfo,
reinterpret_cast<struct sockaddr*>(&addr),
NI_NAMEREQD);
int err = 0;
if (!env->permission()->is_granted(
env, permission::PermissionScope::kNet, ip.ToStringView()))
[[unlikely]] {
req_wrap->InsufficientPermissionError(*ip);
} else {
err = req_wrap->Dispatch(uv_getnameinfo,
AfterGetNameInfo,
reinterpret_cast<struct sockaddr*>(&addr),
NI_NAMEREQD);
}

if (err == 0)
// Release ownership of the pointer allowing the ownership to be transferred
USE(req_wrap.release());
Expand Down
28 changes: 28 additions & 0 deletions src/cares_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "memory_tracker.h"
#include "node.h"
#include "node_internals.h"
#include "permission/permission.h"
#include "util.h"

#include "ares.h"
Expand Down Expand Up @@ -214,6 +215,8 @@ class GetNameInfoReqWrap final : public ReqWrap<uv_getnameinfo_t> {
public:
GetNameInfoReqWrap(Environment* env, v8::Local<v8::Object> req_wrap_obj);

SET_INSUFFICIENT_PERMISSION_ERROR_CALLBACK(permission::PermissionScope::kNet)

SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(GetNameInfoReqWrap)
SET_SELF_SIZE(GetNameInfoReqWrap)
Expand Down Expand Up @@ -249,6 +252,15 @@ class QueryWrap final : public AsyncWrap {
void AresQuery(const char* name,
ares_dns_class_t dnsclass,
ares_dns_rec_type_t type) {
permission::PermissionScope scope = permission::PermissionScope::kNet;
Environment* env_holder = env();

if (!env_holder->permission()->is_granted(env_holder, scope, name))
[[unlikely]] {
QueuePermissionModelResponseCallback(name);
return;
}

channel_->EnsureServers();
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(
TRACING_CATEGORY_NODE2(dns, native), trace_name_, this,
Expand All @@ -262,6 +274,8 @@ class QueryWrap final : public AsyncWrap {
nullptr);
}

SET_INSUFFICIENT_PERMISSION_ERROR_CALLBACK(permission::PermissionScope::kNet)

void ParseError(int status) {
CHECK_NE(status, ARES_SUCCESS);
v8::HandleScope handle_scope(env()->isolate());
Expand Down Expand Up @@ -356,6 +370,20 @@ class QueryWrap final : public AsyncWrap {
wrap->QueueResponseCallback(status);
}

void QueuePermissionModelResponseCallback(const char* resource) {
BaseObjectPtr<QueryWrap<Traits>> strong_ref{this};
const std::string res{resource};
env()->SetImmediate([this, strong_ref, res](Environment*) {
InsufficientPermissionError(res);

// Delete once strong_ref goes out of scope.
Detach();
});

channel_->set_query_last_ok(true);
channel_->ModifyActivityQueryCount(-1);
}

void QueueResponseCallback(int status) {
BaseObjectPtr<QueryWrap<Traits>> strong_ref{this};
env()->SetImmediate([this, strong_ref](Environment*) {
Expand Down
4 changes: 4 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,10 @@ Environment::Environment(IsolateData* isolate_data,
options_->allow_fs_write,
permission::PermissionScope::kFileSystemWrite);
}

if (options_->allow_net) {
permission()->Apply(this, {"*"}, permission::PermissionScope::kNet);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"allow use of child process when any permissions are set",
&EnvironmentOptions::allow_child_process,
kAllowedInEnvvar);
AddOption("--allow-net",
"allow use of network when any permissions are set",
&EnvironmentOptions::allow_net,
kAllowedInEnvvar);
AddOption("--allow-wasi",
"allow wasi when any permissions are set",
&EnvironmentOptions::allow_wasi,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> allow_fs_write;
bool allow_addons = false;
bool allow_child_process = false;
bool allow_net = false;
bool allow_wasi = false;
bool allow_worker_threads = false;
bool experimental_repl_await = true;
Expand Down
23 changes: 23 additions & 0 deletions src/permission/net_permission.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include "net_permission.h"

#include <iostream>
#include <string>

namespace node {

namespace permission {

void NetPermission::Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) {
allow_net_ = true;
}

bool NetPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
return allow_net_;
}

} // namespace permission
} // namespace node
Loading
Loading
0