Skip to content

Commit

Permalink
permission,net,dgram: add permission for net
Browse files Browse the repository at this point in the history
  • Loading branch information
theanarkh committed Jul 11, 2024
1 parent 3a3dfbd commit 8b0d61d
Show file tree
Hide file tree
Showing 18 changed files with 1,038 additions and 10 deletions.
54 changes: 54 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,57 @@ Error: Access to this API has been restricted
}
```

### `--allow-net-udp`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development
When using the [Permission Model][], the process will not be able to create,
receive, and send UDP packages by default. Attempts to do so will throw an
`ERR_ACCESS_DENIED` unless the user explicitly passes the `--allow-net-udp` flag
when starting Node.js.

The argument format is `--allow-net-udp=domain_or_ip[/netmask][:port]`.
The valid arguments are:

* `*` - To allow all operations.
* `--allow-net-udp=nodejs.org`
* `--allow-net-udp=127.0.0.1`
* `--allow-net-udp=127.0.0.1:8888`
* `--allow-net-udp=127.0.0.1:*`
* `--allow-net-udp=*:9999`
* `--allow-net-udp=127.0.0.1/24:*`
* `--allow-net-udp=127.0.0.1/255.255.255.0:*`
* `--allow-net-udp=127.0.0.1:8080 --allow-net-udp=127.0.0.1:9090`
* `--allow-net-udp=127.0.0.1:8080,localhost:9090`

Example:

```js
const dgram = require('node:dgram');
dgram.createSocket('udp4').bind(9297, '127.0.0.1')
```

```console
$ node --experimental-permission --allow-fs-read=./index.js index.js
node:events:498
throw er; // Unhandled 'error' event
^

Error [ERR_ACCESS_DENIED]: Access to this API has been restricted. Permission: bind to 127.0.0.1/9297
at node:dgram:379:18
at process.processTicksAndRejections (node:internal/process/task_queues:77:11)
Emitted 'error' event on Socket instance at:
at afterDns (node:dgram:337:12)
at node:dgram:379:9
at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {
code: 'ERR_ACCESS_DENIED'
}
```

### `--build-snapshot`

<!-- YAML
Expand Down Expand Up @@ -1014,6 +1065,7 @@ following permissions are restricted:
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
* UDP - manageable through [`--allow-net-udp`][] flag

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

Expand Down Expand Up @@ -2835,6 +2887,7 @@ one is included in the list below.
* `--allow-child-process`
* `--allow-fs-read`
* `--allow-fs-write`
* `--allow-net-udp`
* `--allow-wasi`
* `--allow-worker`
* `--conditions`, `-C`
Expand Down Expand Up @@ -3390,6 +3443,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-udp`]: #--allow-net-udp
[`--allow-wasi`]: #--allow-wasi
[`--allow-worker`]: #--allow-worker
[`--build-snapshot`]: #--build-snapshot
Expand Down
4 changes: 4 additions & 0 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.
To allow native addons when using permission model, use the [`--allow-addons`][]
flag. For WASI, use the [`--allow-wasi`][] flag.

For UDP, use [`--allow-net-udp`][] flag.

#### Runtime API

When enabling the Permission Model through the [`--experimental-permission`][]
Expand Down Expand Up @@ -583,6 +585,7 @@ There are constraints you need to know before using this system:
* Inspector protocol
* File system access
* WASI
* UDP
* 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 @@ -607,6 +610,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-udp`]: cli.md#--allow-net-udp
[`--allow-wasi`]: cli.md#--allow-wasi
[`--allow-worker`]: cli.md#--allow-worker
[`--experimental-permission`]: cli.md#--experimental-permission
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ Allow execution of WASI when using the permission model.
.It Fl -allow-worker
Allow creating worker threads when using the permission model.
.
.It Fl -allow-net-udp
Allow create, receive, and, send UDP packages when using the permission model.
.
.It Fl -completion-bash
Print source-able bash completion script for Node.js.
.
Expand Down
83 changes: 74 additions & 9 deletions lib/dgram.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const {
ErrnoException,
ExceptionWithHostPort,
codes: {
ERR_ACCESS_DENIED,
ERR_BUFFER_OUT_OF_BOUNDS,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_FD_TYPE,
Expand Down Expand Up @@ -81,6 +82,7 @@ const {

const dc = require('diagnostics_channel');
const udpSocketChannel = dc.channel('udp.socket');
const permission = require('internal/process/permission');

const BIND_STATE_UNBOUND = 0;
const BIND_STATE_BINDING = 1;
Expand Down Expand Up @@ -327,12 +329,9 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
else
address = '::';
}

// Resolve address first
state.handle.lookup(address, (err, ip) => {
const afterDns = (err, ip) => {
if (!state.handle)
return; // Handle has been closed in the mean time

if (err) {
state.bindState = BIND_STATE_UNBOUND;
this.emit('error', err);
Expand Down Expand Up @@ -372,7 +371,22 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {

startListening(this);
}
});
};
if (permission.isEnabled()) {
const resource = `${address}/${port || '*'}`;
if (!permission.has('net.udp', resource)) {
process.nextTick(() => {
afterDns(new ERR_ACCESS_DENIED(
`bind to ${resource}`,
resource,
'NetUDP'

Check failure on line 382 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
));
});
return this;
}
}
// Resolve address first
state.handle.lookup(address, afterDns);

return this;
};
Expand Down Expand Up @@ -413,13 +427,35 @@ function _connect(port, address, callback) {
this.once('connect', callback);

const afterDns = (ex, ip) => {
if (!ex && !address && permission.isEnabled()) {
const resource = `${ip}/${port}`;
if (!permission.has('net.udp', resource)) {
ex = new ERR_ACCESS_DENIED(
`connect to ${resource}`,
resource,
'NetUDP'

Check failure on line 436 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
);
}
}
defaultTriggerAsyncIdScope(
this[async_id_symbol],
doConnect,
ex, this, ip, address, port, callback,
);
};

if (address && permission.isEnabled()) {
const resource = `${address}/${port}`;
if (!permission.has('net.udp', resource)) {
process.nextTick(() => {
afterDns(new ERR_ACCESS_DENIED(
`connect to ${resource}`,

Check failure on line 451 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected indentation of 10 spaces but found 12
resource,

Check failure on line 452 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected indentation of 10 spaces but found 12
'NetUDP'

Check failure on line 453 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected indentation of 10 spaces but found 12

Check failure on line 453 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
));

Check failure on line 454 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected indentation of 8 spaces but found 10
});
return;
}
}
state.handle.lookup(address, afterDns);
}

Expand All @@ -430,9 +466,13 @@ function doConnect(ex, self, ip, address, port, callback) {
return;

if (!ex) {
const err = state.handle.connect(ip, port);
if (err) {
ex = new ExceptionWithHostPort(err, 'connect', address, port);
try {
const err = state.handle.connect(ip, port);
if (err) {
ex = new ExceptionWithHostPort(err, 'connect', address, port);
}
} catch (e) {
ex = e;
}
}

Expand Down Expand Up @@ -663,6 +703,17 @@ Socket.prototype.send = function(buffer,
}

const afterDns = (ex, ip) => {
// If we have not checked before dns, check it now
if (!ex && !connected && !address && permission.isEnabled()) {
const resource = `${ip}/${port}`;
if (!permission.has('net.udp', resource)) {
ex = new ERR_ACCESS_DENIED(
`send to ${resource}`,
resource,
'NetUDP'

Check failure on line 713 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
);
}
}
defaultTriggerAsyncIdScope(
this[async_id_symbol],
doSend,
Expand All @@ -671,6 +722,20 @@ Socket.prototype.send = function(buffer,
};

if (!connected) {
// If address is not empty, check it
if (address && permission.isEnabled()) {
const resource = `${address}/${port}`;
if (!permission.has('net.udp', resource)) {
process.nextTick(() => {
afterDns(new ERR_ACCESS_DENIED(
`send to ${resource}`,
resource,
'NetUDP'

Check failure on line 733 in lib/dgram.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
));
});
return;
}
}
state.handle.lookup(address, afterDns);
} else {
afterDns(null, null);
Expand Down
1 change: 1 addition & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ function initializePermission() {
'--allow-child-process',
'--allow-wasi',
'--allow-worker',
'--allow-net-udp',
];
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
const value = getOptionValue(flag);
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,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 @@ -283,6 +284,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
4 changes: 4 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,10 @@ Environment::Environment(IsolateData* isolate_data,
options_->allow_fs_write,
permission::PermissionScope::kFileSystemWrite);
}
if (!options_->allow_net_udp.empty()) {
permission()->Apply(
this, options_->allow_net_udp, permission::PermissionScope::kNetUDP);
}
}
}

Expand Down
47 changes: 47 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "env-inl.h"
#include "node_binding.h"
#include "permission/net_permission.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_sea.h"
Expand Down Expand Up @@ -224,6 +225,47 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,

debug_options_.CheckOptions(errors, argv);
#endif // HAVE_INSPECTOR

// The flag --experimental_permission maybe set after --allow-net-udp
if (allow_net_udp.size() > 0) {
using std::string_view_literals::operator""sv;
// Allow multiple item in one flag, such as --allow-net-udp=127.0.0.1,localhost
for (const std::string& res : allow_net_udp) {
const std::vector<std::string_view> addresses = SplitString(res, ","sv);
for (const auto& address : addresses) {
if (address != "*"sv && address != "*:*"sv) {
// Parse string to get IP, port and netmask
std::unique_ptr<PermissionAddressInfo> result = ParseAddress(address);
// Address can be *, host and IP, do we need to check it ?
// Check port
if (result->port != "*" && (
!IsDigit(result->port) ||
std::stoi(result->port) > 0xFFFF
)) {
errors->push_back("invalid port: " + result->port);
}
// Check netmask
if (!result->netmask.empty()) {
int ip_version = IsIP(result->address);
if (ip_version == 0) {
errors->push_back("can not use netmask when IP is not an IPV4 or IPV6 address");
} else if (IsDigit(result->netmask)) { // 127.0.0.1/24
int netmask_len = std::stoi(result->netmask);
if ((ip_version == 4 && netmask_len > 32) ||
(ip_version == 6 && netmask_len > 128)) {
errors->push_back("invalid netmask: " + result->netmask);
}
} else { // 127.0.0.1/255.0.0.0
int netmask_ip_version = IsIP(result->netmask);
if (netmask_ip_version == 0 || netmask_ip_version != ip_version) {
errors->push_back("invalid netmask: " + result->netmask);
}
}
}
}
}
}
}
}

namespace options_parser {
Expand Down Expand Up @@ -476,6 +518,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"allow worker threads when any permissions are set",
&EnvironmentOptions::allow_worker_threads,
kAllowedInEnvvar);
AddOption("--allow-net-udp",
"allow host:port or ip:port to bind and connect by UDP socket "
"when any permissions are set",
&EnvironmentOptions::allow_net_udp,
kAllowedInEnvvar);
AddOption("--experimental-repl-await",
"experimental await keyword support in REPL",
&EnvironmentOptions::experimental_repl_await,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class EnvironmentOptions : public Options {
bool allow_child_process = false;
bool allow_wasi = false;
bool allow_worker_threads = false;
std::vector<std::string> allow_net_udp;
bool experimental_repl_await = true;
bool experimental_vm_modules = false;
bool expose_internals = false;
Expand Down
Loading

0 comments on commit 8b0d61d

Please sign in to comment.