From ec5672e3ec35b56e0b9048e5edfc799bd62d3f4d Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 17 Aug 2026 17:57:11 -0300 Subject: [PATCH] permission: guard UDP handle adoption Signed-off-by: RafaelGSS --- src/udp_wrap.cc | 14 ++ src/udp_wrap.h | 1 + .../test-permission-net-udp-handle.js | 135 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 test/parallel/test-permission-net-udp-handle.js diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index b5e732dd6f1c..e75f2694e708 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -325,6 +325,7 @@ void UDPWrap::DoBind(const FunctionCallbackInfo& args, int family) { err = uv_udp_bind(&wrap->handle_, reinterpret_cast(&addr_storage), flags); + if (err == 0) wrap->net_permission_granted_ = true; } if (err == 0) @@ -358,6 +359,7 @@ void UDPWrap::DoConnect(const FunctionCallbackInfo& args, int family) { if (err == 0) { err = uv_udp_connect(&wrap->handle_, reinterpret_cast(&addr_storage)); + if (err == 0) wrap->net_permission_granted_ = true; } args.GetReturnValue().Set(err); @@ -368,9 +370,14 @@ void UDPWrap::Open(const FunctionCallbackInfo& args) { UDPWrap* wrap; ASSIGN_OR_RETURN_UNWRAP( &wrap, args.This(), args.GetReturnValue().Set(UV_EBADF)); + Environment* env = wrap->env(); + + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kNet, ""); + CHECK(args[0]->IsNumber()); int fd = FromV8Value(args[0]); int err = uv_udp_open(&wrap->handle_, fd); + if (err == 0) wrap->net_permission_granted_ = true; args.GetReturnValue().Set(err); } @@ -697,6 +704,13 @@ void UDPWrapBase::RecvStart(const FunctionCallbackInfo& args) { int UDPWrap::RecvStart() { if (IsHandleClosing()) return UV_EBADF; + + if (!net_permission_granted_) { + THROW_IF_INSUFFICIENT_PERMISSIONS( + env(), permission::PermissionScope::kNet, "", UV_EACCES); + net_permission_granted_ = true; + } + int err = uv_udp_recv_start(&handle_, OnAlloc, OnRecv); // UV_EALREADY means that the socket is already bound but that's okay if (err == UV_EALREADY) diff --git a/src/udp_wrap.h b/src/udp_wrap.h index c0914dbf3a7f..5a26cace2e15 100644 --- a/src/udp_wrap.h +++ b/src/udp_wrap.h @@ -214,6 +214,7 @@ class UDPWrap final : public HandleWrap, uv_udp_t handle_; + bool net_permission_granted_ = false; bool current_send_has_callback_; v8::Local current_send_req_wrap_; }; diff --git a/test/parallel/test-permission-net-udp-handle.js b/test/parallel/test-permission-net-udp-handle.js new file mode 100644 index 000000000000..b6925421f829 --- /dev/null +++ b/test/parallel/test-permission-net-udp-handle.js @@ -0,0 +1,135 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (common.isWindows) { + common.skip('Sending dgram sockets to child processes is not supported'); +} + +const assert = require('assert'); +const dgram = require('dgram'); +const { fork, spawn } = require('child_process'); + +const mode = process.argv[2]; +const kStateSymbol = mode === undefined ? + require('internal/dgram').kStateSymbol : undefined; + +if (mode === 'ipc-denied') { + process.on('message', common.mustNotCall()); + process.send('ready'); +} else if (mode === 'ipc-drop') { + process.once('message', common.mustCall((message, handle) => { + assert.strictEqual(message, 'socket'); + assert(process.permission.has('net')); + + process.permission.drop('net'); + assert(!process.permission.has('net')); + + handle.once('message', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'after-drop'); + handle.close(); + process.send('received'); + })); + process.send('receiving'); + })); + process.send('ready'); +} else { + const deniedSocket = dgram.createSocket('udp4'); + deniedSocket.bind(0, '127.0.0.1', common.mustCall(() => { + const child = fork(__filename, ['ipc-denied'], { + execArgv: ['--permission', '--allow-fs-read=*'], + silent: true, + }); + let stderr = ''; + + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => { stderr += chunk; }); + child.once('message', common.mustCall((message) => { + assert.strictEqual(message, 'ready'); + child.send('socket', deniedSocket); + })); + child.once('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + assert.match(stderr, /ERR_ACCESS_DENIED/); + assert.match(stderr, /permission: 'Net'/); + deniedSocket.close(); + })); + })); + + const droppedSocket = dgram.createSocket('udp4'); + const sender = dgram.createSocket('udp4'); + droppedSocket.bind(0, '127.0.0.1', common.mustCall(() => { + const child = fork(__filename, ['ipc-drop'], { + execArgv: [ + '--permission', + '--allow-net', + '--allow-fs-read=*', + ], + }); + let timer; + + child.on('message', common.mustCall((message) => { + if (message === 'ready') { + child.send('socket', droppedSocket); + } else if (message === 'receiving') { + const { port } = droppedSocket.address(); + timer = setInterval(() => { + sender.send('after-drop', port, '127.0.0.1'); + }, 10); + } else { + assert.strictEqual(message, 'received'); + clearInterval(timer); + sender.close(); + droppedSocket.close(); + child.disconnect(); + } + }, 3)); + child.once('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + })); + + const fdSocket = dgram.createSocket('udp4'); + fdSocket.bind(0, '127.0.0.1', common.mustCall(() => { + const source = ` + const assert = require('node:assert'); + const dgram = require('node:dgram'); + const socket = dgram.createSocket('udp4'); + let error; + try { + socket.bind({ fd: 3 }); + } catch (err) { + error = err; + } + if (error) { + assert.strictEqual(error.code, 'ERR_ACCESS_DENIED'); + assert.strictEqual(error.permission, 'Net'); + process.exit(0); + } + process.exit(1); + `; + const child = spawn( + process.execPath, + ['--permission', '--eval', source], + { + stdio: [ + 'ignore', + 'ignore', + 'pipe', + fdSocket[kStateSymbol].handle.fd, + ], + }, + ); + let stderr = ''; + + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => { stderr += chunk; }); + child.once('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0, stderr); + assert.strictEqual(signal, null); + fdSocket.close(); + })); + })); +}