Access control / Authorization bypass
Description
Summary of the fix:
- The commit adds a permission check for Net scope in PipeWrap::Bind and PipeWrap::Listen. Specifically, it invokes THROW_IF_INSUFFICIENT_PERMISSIONS with permission scope Net (and, for Bind, uses the socket/path name as context) before binding or listening on sockets. This enforces access control for net-related operations (including UNIX domain sockets) and prevents unprivileged code from binding/listening to sockets without the Net permission.
What was changed:
- In src/pipe_wrap.cc, Bind now creates the socket name using the Environment and checks NET permission before calling uv_pipe_bind2.
- Listen now checks NET permission before calling uv_listen.
- Tests were added to ensure that unprivileged attempts to bind/listen to UNIX domain sockets throw ERR_ACCESS_DENIED with permission Net.
Reason this constitutes a real vulnerability fix:
- Prior to this patch, non-privileged code could potentially bind/listen on UNIX domain sockets or network listeners without a permission gate, enabling an authorization bypass around socket creation and binding. By introducing a Net permission check, Node.js now enforces explicit permission to perform net-related operations, aligning with a more robust authorization model and preventing unauthorized service binding or unauthorized listening on sockets as described in the CVE.
Affected behavior prior to patch (assumed):
- Unprivileged code could call net.createServer().listen('/path/to/socket') or tls.createServer().listen('/path/to/socket') and bind to a UNIX socket without Net permission checks.
Security impact:
- Type: Access control / Authorization bypass for net-related operations (UNIX domain sockets and network listeners).
- Severity: HIGH (because it gates the ability to bind/listen to sockets by permissions, preventing potential privilege escalation or resource binding by unprivileged actors).
- Affected versions: versions prior to 25.9.0 (i.e., < 25.9.0).
Notes:
- The commit references a CVE-2026-21711 and includes tests asserting ERR_ACCESS_DENIED with permission Net for unprivileged attempts.
- The changes are not just a dependency bump or a minor refactor; they introduce a functional security gate and corresponding tests.
Proof of Concept
Proof of Concept (PoC)
This PoC demonstrates the vulnerability path before the fix (attempting to bind/listen on a UNIX socket as an unprivileged user). On a vulnerable Node build, this would succeed; after the fix, it should fail with ERR_ACCESS_DENIED due to missing Net permission.
PoC 1: net Unix socket listen (unprivileged environment)
// poc-net.js
const net = require('net');
const fs = require('fs');
const socketPath = '/tmp/poc-server.sock';
try {
if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
} catch (e) {
// ignore
}
try {
const s = net.createServer(() => {});
s.listen(socketPath, () => {
console.log('LISTENING on', socketPath);
});
} catch (err) {
console.error('Error:', err.code, 'permission:', err.permission);
process.exit(1);
}
# How to run (pre-fix vs post-fix)
# 1) Run as a non-root user in an environment where Net permission gating is not yet enforced (pre-fix):
# node poc-net.js
# Expected on vulnerable builds: "LISTENING on /tmp/poc-server.sock" and ability to accept connections.
# 2) Run in a fixed Node.js build (post-fix):
# node poc-net.js
# Expected: throws an error (ERR_ACCESS_DENIED) indicating missing Net permission.
PoC 2: TLS UNIX socket listen (unprivileged environment)
// poc-tls-net.js
const tls = require('tls');
try {
const s = tls.createServer({ /* key/cert omitted for brevity in PoC */ }).listen('/tmp/poc-tls.sock');
console.log('LISTENING on /tmp/poc-tls.sock');
} catch (err) {
console.error('Error:', err.code, 'permission:', err.permission);
process.exit(1);
}
# Run:
# node poc-tls-net.js
# Expect similar ERR_ACCESS_DENIED behavior on patched builds.
Commit Details
Author: RafaelGSS
Date: 2026-02-18 16:37 UTC
Message:
permission: include permission check to pipe_wrap.cc
PR-URL: https://github.com/nodejs-private/node-private/pull/820
Refs: https://hackerone.com/reports/3559715
Reviewed-By: Santiago Gimeno <santiago.gimeno@gmail.com>
CVE-ID: CVE-2026-21711
Triage Assessment
Vulnerability Type: Access control / Authorization bypass
Confidence: HIGH
Reasoning:
The patch adds a permission check (Net scope) before binding and listening on sockets in PipeWrap, and accompanying tests ensure that unprivileged attempts are blocked with ERR_ACCESS_DENIED. This directly enforces access control for net-related operations, preventing unauthorized use of UNIX domain sockets or network listeners. The commit references a CVE ID, indicating a tracked security vulnerability.
Verification Assessment
Vulnerability Type: Access control / Authorization bypass
Confidence: HIGH
Affected Versions: < 25.9.0
Code Diff
diff --git a/src/pipe_wrap.cc b/src/pipe_wrap.cc
index 770f0847aec59f..5100b0fed17455 100644
--- a/src/pipe_wrap.cc
+++ b/src/pipe_wrap.cc
@@ -162,7 +162,10 @@ PipeWrap::PipeWrap(Environment* env,
void PipeWrap::Bind(const FunctionCallbackInfo<Value>& args) {
PipeWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
- node::Utf8Value name(args.GetIsolate(), args[0]);
+ Environment* env = wrap->env();
+ node::Utf8Value name(env->isolate(), args[0]);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kNet, name.ToStringView());
int err =
uv_pipe_bind2(&wrap->handle_, *name, name.length(), UV_PIPE_NO_TRUNCATE);
args.GetReturnValue().Set(err);
@@ -193,6 +196,7 @@ void PipeWrap::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);
args.GetReturnValue().Set(err);
diff --git a/test/parallel/test-permission-net-uds.js b/test/parallel/test-permission-net-uds.js
index 7024c9ff6d3b16..436757e9d8b3cb 100644
--- a/test/parallel/test-permission-net-uds.js
+++ b/test/parallel/test-permission-net-uds.js
@@ -29,3 +29,21 @@ const tls = require('tls');
client.on('connect', common.mustNotCall('TCP connection should be blocked'));
}
+
+{
+ assert.throws(() => {
+ net.createServer().listen('/tmp/perm-server.sock');
+ }, {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'Net',
+ });
+}
+
+{
+ assert.throws(() => {
+ tls.createServer().listen('/tmp/perm-tls-server.sock');
+ }, {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'Net',
+ });
+}