mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
src,permission: add --allow-net permission
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: https://github.com/nodejs/node/pull/58517 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
parent
07220230d9
commit
462c74181d
33 changed files with 681 additions and 19 deletions
|
@ -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
|
||||
|
@ -1943,6 +1975,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
|
||||
|
@ -3297,6 +3330,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`
|
||||
|
@ -3899,6 +3933,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
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -197,6 +198,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
|
||||
|
@ -227,6 +229,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
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"allow-net": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allow-wasi": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
.
|
||||
|
|
|
@ -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
|
||||
|
@ -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}`;
|
||||
// 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})`;
|
||||
}
|
||||
}
|
||||
if (additional) {
|
||||
details += ` - Local (${additional})`;
|
||||
}
|
||||
|
||||
super(`${syscall} ${code}${details}`);
|
||||
|
||||
this.errno = err;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
@ -614,6 +625,7 @@ function initializePermission() {
|
|||
'--allow-fs-write',
|
||||
'--allow-addons',
|
||||
'--allow-child-process',
|
||||
'--allow-net',
|
||||
'--allow-wasi',
|
||||
'--allow-worker',
|
||||
];
|
||||
|
|
2
node.gyp
2
node.gyp
|
@ -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',
|
||||
|
@ -292,6 +293,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',
|
||||
|
|
|
@ -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)];
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
|
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
@ -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*) {
|
||||
|
|
|
@ -958,6 +958,10 @@ Environment::Environment(IsolateData* isolate_data,
|
|||
options_->allow_fs_write,
|
||||
permission::PermissionScope::kFileSystemWrite);
|
||||
}
|
||||
|
||||
if (options_->allow_net) {
|
||||
permission()->Apply(this, {"*"}, permission::PermissionScope::kNet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -565,6 +565,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,
|
||||
|
|
|
@ -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;
|
||||
|
|
23
src/permission/net_permission.cc
Normal file
23
src/permission/net_permission.cc
Normal file
|
@ -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
|
31
src/permission/net_permission.h
Normal file
31
src/permission/net_permission.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
#ifndef SRC_PERMISSION_NET_PERMISSION_H_
|
||||
#define SRC_PERMISSION_NET_PERMISSION_H_
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <string>
|
||||
#include "permission/permission_base.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
namespace permission {
|
||||
|
||||
class NetPermission 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 allow_net_ = false;
|
||||
};
|
||||
|
||||
} // namespace permission
|
||||
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#endif // SRC_PERMISSION_NET_PERMISSION_H_
|
|
@ -84,6 +84,7 @@ Permission::Permission() : enabled_(false) {
|
|||
std::shared_ptr<PermissionBase> inspector =
|
||||
std::make_shared<InspectorPermission>();
|
||||
std::shared_ptr<PermissionBase> wasi = std::make_shared<WASIPermission>();
|
||||
std::shared_ptr<PermissionBase> net = std::make_shared<NetPermission>();
|
||||
#define V(Name, _, __, ___) \
|
||||
nodes_.insert(std::make_pair(PermissionScope::k##Name, fs));
|
||||
FILESYSTEM_PERMISSIONS(V)
|
||||
|
@ -104,6 +105,10 @@ Permission::Permission() : enabled_(false) {
|
|||
nodes_.insert(std::make_pair(PermissionScope::k##Name, wasi));
|
||||
WASI_PERMISSIONS(V)
|
||||
#undef V
|
||||
#define V(Name, _, __, ___) \
|
||||
nodes_.insert(std::make_pair(PermissionScope::k##Name, net));
|
||||
NET_PERMISSIONS(V)
|
||||
#undef V
|
||||
}
|
||||
|
||||
const char* GetErrorFlagSuggestion(node::permission::PermissionScope perm) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "permission/child_process_permission.h"
|
||||
#include "permission/fs_permission.h"
|
||||
#include "permission/inspector_permission.h"
|
||||
#include "permission/net_permission.h"
|
||||
#include "permission/permission_base.h"
|
||||
#include "permission/wasi_permission.h"
|
||||
#include "permission/worker_permission.h"
|
||||
|
@ -45,6 +46,32 @@ namespace permission {
|
|||
} \
|
||||
} while (0)
|
||||
|
||||
#define ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS( \
|
||||
env, perm_, resource_, args, ...) \
|
||||
do { \
|
||||
if (!env->permission()->is_granted(env, perm_, resource_)) [[unlikely]] { \
|
||||
Local<Value> err_access; \
|
||||
if (permission::CreateAccessDeniedError(env, perm_, resource_) \
|
||||
.ToLocal(&err_access)) { \
|
||||
args.GetReturnValue().Set(err_access); \
|
||||
} else { \
|
||||
args.GetReturnValue().Set(UV_EACCES); \
|
||||
} \
|
||||
return __VA_ARGS__; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define SET_INSUFFICIENT_PERMISSION_ERROR_CALLBACK(scope) \
|
||||
void InsufficientPermissionError(const std::string resource) { \
|
||||
v8::HandleScope handle_scope(env()->isolate()); \
|
||||
v8::Context::Scope context_scope(env()->context()); \
|
||||
v8::Local<v8::Value> arg; \
|
||||
if (!permission::CreateAccessDeniedError(env(), scope, resource) \
|
||||
.ToLocal(&arg)) { \
|
||||
} \
|
||||
MakeCallback(env()->oncomplete_string(), 1, &arg); \
|
||||
}
|
||||
|
||||
class Permission {
|
||||
public:
|
||||
Permission();
|
||||
|
@ -91,6 +118,10 @@ class Permission {
|
|||
bool enabled_;
|
||||
};
|
||||
|
||||
v8::MaybeLocal<v8::Value> CreateAccessDeniedError(Environment* env,
|
||||
PermissionScope perm,
|
||||
const std::string_view& res);
|
||||
|
||||
} // namespace permission
|
||||
|
||||
} // namespace node
|
||||
|
|
|
@ -29,12 +29,15 @@ namespace permission {
|
|||
|
||||
#define INSPECTOR_PERMISSIONS(V) V(Inspector, "inspector", PermissionsRoot, "")
|
||||
|
||||
#define NET_PERMISSIONS(V) V(Net, "net", PermissionsRoot, "--allow-net")
|
||||
|
||||
#define PERMISSIONS(V) \
|
||||
FILESYSTEM_PERMISSIONS(V) \
|
||||
CHILD_PROCESS_PERMISSIONS(V) \
|
||||
WASI_PERMISSIONS(V) \
|
||||
WORKER_THREADS_PERMISSIONS(V) \
|
||||
INSPECTOR_PERMISSIONS(V)
|
||||
INSPECTOR_PERMISSIONS(V) \
|
||||
NET_PERMISSIONS(V)
|
||||
|
||||
#define V(name, _, __, ___) k##name,
|
||||
enum class PermissionScope {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include "node_buffer.h"
|
||||
#include "node_external_reference.h"
|
||||
#include "node_internals.h"
|
||||
#include "permission/permission.h"
|
||||
#include "stream_base-inl.h"
|
||||
#include "stream_wrap.h"
|
||||
#include "util-inl.h"
|
||||
|
@ -245,6 +246,9 @@ void TCPWrap::Bind(
|
|||
ASSIGN_OR_RETURN_UNWRAP(
|
||||
&wrap, args.This(), args.GetReturnValue().Set(UV_EBADF));
|
||||
Environment* env = wrap->env();
|
||||
|
||||
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kNet, "");
|
||||
|
||||
node::Utf8Value ip_address(env->isolate(), args[0]);
|
||||
int port;
|
||||
unsigned int flags = 0;
|
||||
|
@ -285,6 +289,9 @@ void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
|
|||
Environment* env = wrap->env();
|
||||
int backlog;
|
||||
if (!args[0]->Int32Value(env->context()).To(&backlog)) return;
|
||||
|
||||
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kNet, "");
|
||||
|
||||
int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
|
||||
backlog,
|
||||
OnConnection);
|
||||
|
@ -329,6 +336,9 @@ void TCPWrap::Connect(const FunctionCallbackInfo<Value>& args,
|
|||
Local<Object> req_wrap_obj = args[0].As<Object>();
|
||||
node::Utf8Value ip_address(env->isolate(), args[1]);
|
||||
|
||||
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
|
||||
env, permission::PermissionScope::kNet, ip_address.ToStringView(), args);
|
||||
|
||||
T addr;
|
||||
int err = uv_ip_addr(*ip_address, &addr);
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "node_errors.h"
|
||||
#include "node_external_reference.h"
|
||||
#include "node_sockaddr-inl.h"
|
||||
#include "permission/permission.h"
|
||||
#include "req_wrap-inl.h"
|
||||
#include "util-inl.h"
|
||||
|
||||
|
@ -307,6 +308,13 @@ void UDPWrap::DoBind(const FunctionCallbackInfo<Value>& args, int family) {
|
|||
CHECK_EQ(args.Length(), 3);
|
||||
|
||||
node::Utf8Value address(args.GetIsolate(), args[0]);
|
||||
|
||||
// Check for network permission
|
||||
Environment* env = wrap->env();
|
||||
|
||||
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
|
||||
env, permission::PermissionScope::kNet, address.ToStringView(), args);
|
||||
|
||||
Local<Context> ctx = args.GetIsolate()->GetCurrentContext();
|
||||
uint32_t port, flags;
|
||||
if (!args[1]->Uint32Value(ctx).To(&port) ||
|
||||
|
@ -332,6 +340,13 @@ void UDPWrap::DoConnect(const FunctionCallbackInfo<Value>& args, int family) {
|
|||
ASSIGN_OR_RETURN_UNWRAP(
|
||||
&wrap, args.This(), args.GetReturnValue().Set(UV_EBADF));
|
||||
|
||||
// Check for network permission
|
||||
Environment* env = wrap->env();
|
||||
THROW_IF_INSUFFICIENT_PERMISSIONS(env,
|
||||
permission::PermissionScope::kNet,
|
||||
"",
|
||||
args.GetReturnValue().Set(UV_EACCES));
|
||||
|
||||
CHECK_EQ(args.Length(), 2);
|
||||
|
||||
node::Utf8Value address(args.GetIsolate(), args[0]);
|
||||
|
@ -523,6 +538,12 @@ void UDPWrap::DoSend(const FunctionCallbackInfo<Value>& args, int family) {
|
|||
ASSIGN_OR_RETURN_UNWRAP(
|
||||
&wrap, args.This(), args.GetReturnValue().Set(UV_EBADF));
|
||||
|
||||
// Check for network permission
|
||||
THROW_IF_INSUFFICIENT_PERMISSIONS(env,
|
||||
permission::PermissionScope::kNet,
|
||||
"",
|
||||
args.GetReturnValue().Set(UV_EACCES));
|
||||
|
||||
CHECK(args.Length() == 4 || args.Length() == 6);
|
||||
CHECK(args[0]->IsObject());
|
||||
CHECK(args[1]->IsArray());
|
||||
|
|
9
test/fixtures/permission/net-fetch.js
vendored
Normal file
9
test/fixtures/permission/net-fetch.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../../common');
|
||||
const assert = require('assert');
|
||||
const url = process.env.URL;
|
||||
|
||||
fetch(url).catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.cause.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
15
test/fixtures/permission/net-http.js
vendored
Normal file
15
test/fixtures/permission/net-http.js
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../../common');
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
|
||||
const url = process.env.URL;
|
||||
|
||||
{
|
||||
const req = http.get(url, common.mustNotCall('HTTP request should be blocked'));
|
||||
|
||||
req.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
19
test/fixtures/permission/net-https.js
vendored
Normal file
19
test/fixtures/permission/net-https.js
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../../common');
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
|
||||
const url = process.env.URL;
|
||||
|
||||
{
|
||||
const options = {
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
|
||||
const req = https.get(url, options, common.mustNotCall('HTTPS request should be blocked'));
|
||||
|
||||
req.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
35
test/fixtures/permission/net-tcp.js
vendored
Normal file
35
test/fixtures/permission/net-tcp.js
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../../common');
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const port = parseInt(process.env.PORT, 10);
|
||||
|
||||
{
|
||||
const client = net.connect({
|
||||
port,
|
||||
host,
|
||||
});
|
||||
|
||||
client.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
|
||||
client.on('connect', common.mustNotCall('TCP connection should be blocked'));
|
||||
}
|
||||
|
||||
{
|
||||
const client = net.connect({
|
||||
port,
|
||||
host: '127.0.0.1',
|
||||
});
|
||||
|
||||
client.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
assert.strictEqual(err.syscall, 'connect');
|
||||
}));
|
||||
|
||||
client.on('connect', common.mustNotCall('TCP connection should be blocked'));
|
||||
}
|
|
@ -25,3 +25,14 @@ const assert = require('assert');
|
|||
{
|
||||
assert.ok(!process.permission.has('FileSystemWrite', Buffer.from('reference')));
|
||||
}
|
||||
|
||||
{
|
||||
assert.ok(!process.permission.has('fs'));
|
||||
assert.ok(process.permission.has('fs.read'));
|
||||
assert.ok(!process.permission.has('fs.write'));
|
||||
assert.ok(!process.permission.has('wasi'));
|
||||
assert.ok(!process.permission.has('worker'));
|
||||
assert.ok(!process.permission.has('inspector'));
|
||||
assert.ok(!process.permission.has('net'));
|
||||
// TODO(rafaelgss): add addon
|
||||
}
|
||||
|
|
9
test/parallel/test-permission-net-allowed.js
Normal file
9
test/parallel/test-permission-net-allowed.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Flags: --permission --allow-net --allow-fs-read=*
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const assert = require('node:assert');
|
||||
|
||||
{
|
||||
assert.ok(process.permission.has('net'));
|
||||
}
|
94
test/parallel/test-permission-net-dns.js
Normal file
94
test/parallel/test-permission-net-dns.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Flags: --permission --allow-fs-read=*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const dns = require('dns');
|
||||
const { Resolver } = dns.promises;
|
||||
|
||||
{
|
||||
dns.lookup('localhost', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
dns.promises.lookup('localhost').catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
dns.resolve('localhost', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
dns.resolve('come.on.fhqwhgads.test', (err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
});
|
||||
dns.promises.resolve('localhost').catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
const resolver = new Resolver();
|
||||
resolver.resolve('localhost').catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
const ip = '8.8.8.8';
|
||||
dns.reverse(ip, common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
|
||||
dns.promises.reverse(ip).catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
dns.lookup('127.0.0.1', common.mustSucceed((address, family) => {
|
||||
assert.strictEqual(typeof address, 'string');
|
||||
assert.ok(family === 4 || family === 6);
|
||||
}));
|
||||
|
||||
dns.lookup('127.0.0.1', { family: 4 }, common.mustSucceed((address, family) => {
|
||||
assert.strictEqual(typeof address, 'string');
|
||||
assert.strictEqual(family, 4);
|
||||
}));
|
||||
|
||||
dns.lookup('127.0.0.1', { all: true }, common.mustSucceed((results) => {
|
||||
assert(Array.isArray(results));
|
||||
assert(results.length > 0);
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
dns.resolve4('localhost', common.mustCall((err, addresses) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers(['127.0.0.1']); // Do not throw
|
||||
|
||||
resolver.resolve('localhost').catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
dns.lookupService('127.0.0.1', 80, common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
|
||||
dns.lookupService('8.8.8.8', 80, common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
dns.promises.lookupService('127.0.0.1', 80).catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
const resolver = new Resolver();
|
||||
resolver.getServers(); // Do not throw
|
||||
}
|
42
test/parallel/test-permission-net-fetch.js
Normal file
42
test/parallel/test-permission-net-fetch.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const file = fixtures.path('permission', 'net-fetch.js');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end('Hello');
|
||||
}).listen(0, common.mustCall(() => {
|
||||
const port = server.address().port;
|
||||
const url = `http://localhost:${port}`;
|
||||
|
||||
const { status, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
URL: url,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.close();
|
||||
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
}));
|
41
test/parallel/test-permission-net-http.js
Normal file
41
test/parallel/test-permission-net-http.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const file = fixtures.path('permission', 'net-http.js');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end('Hello');
|
||||
}).listen(0, common.mustCall(() => {
|
||||
const port = server.address().port;
|
||||
const url = `http://localhost:${port}`;
|
||||
|
||||
const { status, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
URL: url,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.close();
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
}));
|
48
test/parallel/test-permission-net-https.js
Normal file
48
test/parallel/test-permission-net-https.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) { common.skip('missing crypto'); };
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const file = fixtures.path('permission', 'net-https.js');
|
||||
|
||||
const options = {
|
||||
key: fs.readFileSync(fixtures.path('keys', 'agent1-key.pem')),
|
||||
cert: fs.readFileSync(fixtures.path('keys', 'agent1-cert.pem'))
|
||||
};
|
||||
|
||||
const server = https.createServer(options, (req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end('Hello');
|
||||
}).listen(0, common.mustCall(() => {
|
||||
const port = server.address().port;
|
||||
const url = `https://localhost:${port}`;
|
||||
|
||||
const { status, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
URL: url,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.close();
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
}));
|
38
test/parallel/test-permission-net-tcp.js
Normal file
38
test/parallel/test-permission-net-tcp.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const file = fixtures.path('permission', 'net-tcp.js');
|
||||
|
||||
const server = net.createServer().listen(0, common.mustCall(() => {
|
||||
const port = server.address().port;
|
||||
|
||||
const { status, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: `${port}`,
|
||||
HOST: 'localhost',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.close();
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
}));
|
22
test/parallel/test-permission-net-udp.js
Normal file
22
test/parallel/test-permission-net-udp.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Flags: --permission --allow-fs-read=*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
|
||||
{
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
socket.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'ERR_ACCESS_DENIED');
|
||||
}));
|
||||
|
||||
socket.bind(0, common.mustNotCall());
|
||||
}
|
7
test/parallel/test-permission-net-warning.js
Normal file
7
test/parallel/test-permission-net-warning.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Flags: --permission --allow-net --allow-fs-read=*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
common.expectWarning('ExperimentalWarning',
|
||||
'The flag --allow-net is under experimental phase.');
|
18
test/parallel/test-permission-net-websocket.js
Normal file
18
test/parallel/test-permission-net-websocket.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Flags: --permission --allow-fs-read=*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('no crypto');
|
||||
}
|
||||
|
||||
const wsUrl = 'ws://nodejs.org';
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.addEventListener('open', common.mustNotCall('WebSocket connection should be blocked'));
|
||||
// WebSocket implementation doesn't expose the Node.js specific errors
|
||||
// so ERR_ACCESS_DENIED won't be "caught" explicitly.
|
||||
// For now, let's just assert the error
|
||||
// TODO(rafaelgss): make this test more comprehensive
|
||||
ws.addEventListener('error', common.mustCall());
|
Loading…
Add table
Add a link
Reference in a new issue