Skip to content

Commit 8b0d61d

Browse files
committed
permission,net,dgram: add permission for net
1 parent 3a3dfbd commit 8b0d61d

File tree

18 files changed

+1038
-10
lines changed

18 files changed

+1038
-10
lines changed

doc/api/cli.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,57 @@ Error: Access to this API has been restricted
357357
}
358358
```
359359

360+
### `--allow-net-udp`
361+
362+
<!-- YAML
363+
added: REPLACEME
364+
-->
365+
366+
> Stability: 1.1 - Active development
367+
368+
When using the [Permission Model][], the process will not be able to create,
369+
receive, and send UDP packages by default. Attempts to do so will throw an
370+
`ERR_ACCESS_DENIED` unless the user explicitly passes the `--allow-net-udp` flag
371+
when starting Node.js.
372+
373+
The argument format is `--allow-net-udp=domain_or_ip[/netmask][:port]`.
374+
The valid arguments are:
375+
376+
* `*` - To allow all operations.
377+
* `--allow-net-udp=nodejs.org`
378+
* `--allow-net-udp=127.0.0.1`
379+
* `--allow-net-udp=127.0.0.1:8888`
380+
* `--allow-net-udp=127.0.0.1:*`
381+
* `--allow-net-udp=*:9999`
382+
* `--allow-net-udp=127.0.0.1/24:*`
383+
* `--allow-net-udp=127.0.0.1/255.255.255.0:*`
384+
* `--allow-net-udp=127.0.0.1:8080 --allow-net-udp=127.0.0.1:9090`
385+
* `--allow-net-udp=127.0.0.1:8080,localhost:9090`
386+
387+
Example:
388+
389+
```js
390+
const dgram = require('node:dgram');
391+
dgram.createSocket('udp4').bind(9297, '127.0.0.1')
392+
```
393+
394+
```console
395+
$ node --experimental-permission --allow-fs-read=./index.js index.js
396+
node:events:498
397+
throw er; // Unhandled 'error' event
398+
^
399+
400+
Error [ERR_ACCESS_DENIED]: Access to this API has been restricted. Permission: bind to 127.0.0.1/9297
401+
at node:dgram:379:18
402+
at process.processTicksAndRejections (node:internal/process/task_queues:77:11)
403+
Emitted 'error' event on Socket instance at:
404+
at afterDns (node:dgram:337:12)
405+
at node:dgram:379:9
406+
at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {
407+
code: 'ERR_ACCESS_DENIED'
408+
}
409+
```
410+
360411
### `--build-snapshot`
361412

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

10181070
### `--experimental-require-module`
10191071

@@ -2835,6 +2887,7 @@ one is included in the list below.
28352887
* `--allow-child-process`
28362888
* `--allow-fs-read`
28372889
* `--allow-fs-write`
2890+
* `--allow-net-udp`
28382891
* `--allow-wasi`
28392892
* `--allow-worker`
28402893
* `--conditions`, `-C`
@@ -3390,6 +3443,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
33903443
[`--allow-child-process`]: #--allow-child-process
33913444
[`--allow-fs-read`]: #--allow-fs-read
33923445
[`--allow-fs-write`]: #--allow-fs-write
3446+
[`--allow-net-udp`]: #--allow-net-udp
33933447
[`--allow-wasi`]: #--allow-wasi
33943448
[`--allow-worker`]: #--allow-worker
33953449
[`--build-snapshot`]: #--build-snapshot

doc/api/permissions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,8 @@ using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.
509509
To allow native addons when using permission model, use the [`--allow-addons`][]
510510
flag. For WASI, use the [`--allow-wasi`][] flag.
511511

512+
For UDP, use [`--allow-net-udp`][] flag.
513+
512514
#### Runtime API
513515

514516
When enabling the Permission Model through the [`--experimental-permission`][]
@@ -583,6 +585,7 @@ There are constraints you need to know before using this system:
583585
* Inspector protocol
584586
* File system access
585587
* WASI
588+
* UDP
586589
* The Permission Model is initialized after the Node.js environment is set up.
587590
However, certain flags such as `--env-file` or `--openssl-config` are designed
588591
to read files before environment initialization. As a result, such flags are
@@ -607,6 +610,7 @@ There are constraints you need to know before using this system:
607610
[`--allow-child-process`]: cli.md#--allow-child-process
608611
[`--allow-fs-read`]: cli.md#--allow-fs-read
609612
[`--allow-fs-write`]: cli.md#--allow-fs-write
613+
[`--allow-net-udp`]: cli.md#--allow-net-udp
610614
[`--allow-wasi`]: cli.md#--allow-wasi
611615
[`--allow-worker`]: cli.md#--allow-worker
612616
[`--experimental-permission`]: cli.md#--experimental-permission

doc/node.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ Allow execution of WASI when using the permission model.
9191
.It Fl -allow-worker
9292
Allow creating worker threads when using the permission model.
9393
.
94+
.It Fl -allow-net-udp
95+
Allow create, receive, and, send UDP packages when using the permission model.
96+
.
9497
.It Fl -completion-bash
9598
Print source-able bash completion script for Node.js.
9699
.

lib/dgram.js

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const {
3838
ErrnoException,
3939
ExceptionWithHostPort,
4040
codes: {
41+
ERR_ACCESS_DENIED,
4142
ERR_BUFFER_OUT_OF_BOUNDS,
4243
ERR_INVALID_ARG_TYPE,
4344
ERR_INVALID_FD_TYPE,
@@ -81,6 +82,7 @@ const {
8182

8283
const dc = require('diagnostics_channel');
8384
const udpSocketChannel = dc.channel('udp.socket');
85+
const permission = require('internal/process/permission');
8486

8587
const BIND_STATE_UNBOUND = 0;
8688
const BIND_STATE_BINDING = 1;
@@ -327,12 +329,9 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
327329
else
328330
address = '::';
329331
}
330-
331-
// Resolve address first
332-
state.handle.lookup(address, (err, ip) => {
332+
const afterDns = (err, ip) => {
333333
if (!state.handle)
334334
return; // Handle has been closed in the mean time
335-
336335
if (err) {
337336
state.bindState = BIND_STATE_UNBOUND;
338337
this.emit('error', err);
@@ -372,7 +371,22 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
372371

373372
startListening(this);
374373
}
375-
});
374+
};
375+
if (permission.isEnabled()) {
376+
const resource = `${address}/${port || '*'}`;
377+
if (!permission.has('net.udp', resource)) {
378+
process.nextTick(() => {
379+
afterDns(new ERR_ACCESS_DENIED(
380+
`bind to ${resource}`,
381+
resource,
382+
'NetUDP'
383+
));
384+
});
385+
return this;
386+
}
387+
}
388+
// Resolve address first
389+
state.handle.lookup(address, afterDns);
376390

377391
return this;
378392
};
@@ -413,13 +427,35 @@ function _connect(port, address, callback) {
413427
this.once('connect', callback);
414428

415429
const afterDns = (ex, ip) => {
430+
if (!ex && !address && permission.isEnabled()) {
431+
const resource = `${ip}/${port}`;
432+
if (!permission.has('net.udp', resource)) {
433+
ex = new ERR_ACCESS_DENIED(
434+
`connect to ${resource}`,
435+
resource,
436+
'NetUDP'
437+
);
438+
}
439+
}
416440
defaultTriggerAsyncIdScope(
417441
this[async_id_symbol],
418442
doConnect,
419443
ex, this, ip, address, port, callback,
420444
);
421445
};
422-
446+
if (address && permission.isEnabled()) {
447+
const resource = `${address}/${port}`;
448+
if (!permission.has('net.udp', resource)) {
449+
process.nextTick(() => {
450+
afterDns(new ERR_ACCESS_DENIED(
451+
`connect to ${resource}`,
452+
resource,
453+
'NetUDP'
454+
));
455+
});
456+
return;
457+
}
458+
}
423459
state.handle.lookup(address, afterDns);
424460
}
425461

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

432468
if (!ex) {
433-
const err = state.handle.connect(ip, port);
434-
if (err) {
435-
ex = new ExceptionWithHostPort(err, 'connect', address, port);
469+
try {
470+
const err = state.handle.connect(ip, port);
471+
if (err) {
472+
ex = new ExceptionWithHostPort(err, 'connect', address, port);
473+
}
474+
} catch (e) {
475+
ex = e;
436476
}
437477
}
438478

@@ -663,6 +703,17 @@ Socket.prototype.send = function(buffer,
663703
}
664704

665705
const afterDns = (ex, ip) => {
706+
// If we have not checked before dns, check it now
707+
if (!ex && !connected && !address && permission.isEnabled()) {
708+
const resource = `${ip}/${port}`;
709+
if (!permission.has('net.udp', resource)) {
710+
ex = new ERR_ACCESS_DENIED(
711+
`send to ${resource}`,
712+
resource,
713+
'NetUDP'
714+
);
715+
}
716+
}
666717
defaultTriggerAsyncIdScope(
667718
this[async_id_symbol],
668719
doSend,
@@ -671,6 +722,20 @@ Socket.prototype.send = function(buffer,
671722
};
672723

673724
if (!connected) {
725+
// If address is not empty, check it
726+
if (address && permission.isEnabled()) {
727+
const resource = `${address}/${port}`;
728+
if (!permission.has('net.udp', resource)) {
729+
process.nextTick(() => {
730+
afterDns(new ERR_ACCESS_DENIED(
731+
`send to ${resource}`,
732+
resource,
733+
'NetUDP'
734+
));
735+
});
736+
return;
737+
}
738+
}
674739
state.handle.lookup(address, afterDns);
675740
} else {
676741
afterDns(null, null);

lib/internal/process/pre_execution.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ function initializePermission() {
597597
'--allow-child-process',
598598
'--allow-wasi',
599599
'--allow-worker',
600+
'--allow-net-udp',
600601
];
601602
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
602603
const value = getOptionValue(flag);

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
'src/permission/permission.cc',
159159
'src/permission/wasi_permission.cc',
160160
'src/permission/worker_permission.cc',
161+
'src/permission/net_permission.cc',
161162
'src/pipe_wrap.cc',
162163
'src/process_wrap.cc',
163164
'src/signal_wrap.cc',
@@ -283,6 +284,7 @@
283284
'src/permission/permission.h',
284285
'src/permission/wasi_permission.h',
285286
'src/permission/worker_permission.h',
287+
'src/permission/net_permission.h',
286288
'src/pipe_wrap.h',
287289
'src/req_wrap.h',
288290
'src/req_wrap-inl.h',

src/env.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,10 @@ Environment::Environment(IsolateData* isolate_data,
950950
options_->allow_fs_write,
951951
permission::PermissionScope::kFileSystemWrite);
952952
}
953+
if (!options_->allow_net_udp.empty()) {
954+
permission()->Apply(
955+
this, options_->allow_net_udp, permission::PermissionScope::kNetUDP);
956+
}
953957
}
954958
}
955959

src/node_options.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "env-inl.h"
55
#include "node_binding.h"
6+
#include "permission/net_permission.h"
67
#include "node_external_reference.h"
78
#include "node_internals.h"
89
#include "node_sea.h"
@@ -224,6 +225,47 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
224225

225226
debug_options_.CheckOptions(errors, argv);
226227
#endif // HAVE_INSPECTOR
228+
229+
// The flag --experimental_permission maybe set after --allow-net-udp
230+
if (allow_net_udp.size() > 0) {
231+
using std::string_view_literals::operator""sv;
232+
// Allow multiple item in one flag, such as --allow-net-udp=127.0.0.1,localhost
233+
for (const std::string& res : allow_net_udp) {
234+
const std::vector<std::string_view> addresses = SplitString(res, ","sv);
235+
for (const auto& address : addresses) {
236+
if (address != "*"sv && address != "*:*"sv) {
237+
// Parse string to get IP, port and netmask
238+
std::unique_ptr<PermissionAddressInfo> result = ParseAddress(address);
239+
// Address can be *, host and IP, do we need to check it ?
240+
// Check port
241+
if (result->port != "*" && (
242+
!IsDigit(result->port) ||
243+
std::stoi(result->port) > 0xFFFF
244+
)) {
245+
errors->push_back("invalid port: " + result->port);
246+
}
247+
// Check netmask
248+
if (!result->netmask.empty()) {
249+
int ip_version = IsIP(result->address);
250+
if (ip_version == 0) {
251+
errors->push_back("can not use netmask when IP is not an IPV4 or IPV6 address");
252+
} else if (IsDigit(result->netmask)) { // 127.0.0.1/24
253+
int netmask_len = std::stoi(result->netmask);
254+
if ((ip_version == 4 && netmask_len > 32) ||
255+
(ip_version == 6 && netmask_len > 128)) {
256+
errors->push_back("invalid netmask: " + result->netmask);
257+
}
258+
} else { // 127.0.0.1/255.0.0.0
259+
int netmask_ip_version = IsIP(result->netmask);
260+
if (netmask_ip_version == 0 || netmask_ip_version != ip_version) {
261+
errors->push_back("invalid netmask: " + result->netmask);
262+
}
263+
}
264+
}
265+
}
266+
}
267+
}
268+
}
227269
}
228270

229271
namespace options_parser {
@@ -476,6 +518,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
476518
"allow worker threads when any permissions are set",
477519
&EnvironmentOptions::allow_worker_threads,
478520
kAllowedInEnvvar);
521+
AddOption("--allow-net-udp",
522+
"allow host:port or ip:port to bind and connect by UDP socket "
523+
"when any permissions are set",
524+
&EnvironmentOptions::allow_net_udp,
525+
kAllowedInEnvvar);
479526
AddOption("--experimental-repl-await",
480527
"experimental await keyword support in REPL",
481528
&EnvironmentOptions::experimental_repl_await,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class EnvironmentOptions : public Options {
135135
bool allow_child_process = false;
136136
bool allow_wasi = false;
137137
bool allow_worker_threads = false;
138+
std::vector<std::string> allow_net_udp;
138139
bool experimental_repl_await = true;
139140
bool experimental_vm_modules = false;
140141
bool expose_internals = false;

0 commit comments

Comments
 (0)