Permission model bypass via unchecked Unix Domain Socket connections (CVE-2026-21636)
Summary
Node.js permission model fails to enforce network restrictions for Unix Domain Socket connections
Node.js permission model fails to enforce network restrictions for Unix Domain Socket (UDS) connections. With --permission enabled and without --allow-net (or any allowlists), an attacker-controlled URL or socketPath still reaches arbitrary local sockets via net, tls, or undici/fetch. This breaks the security boundary the permission model is meant to provide and enables SSRF-to-local-RCE style impact against local daemons (e.g., Docker API) while the administrator believes networking is blocked.
CVSS
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Source → Sink
deps/undici/src/lib/core/connect.js
Line 51 · options = { path: socketPath }
src/pipe_wrap.cc
Line 216 · PipeWrap::Connect
Attack surface
Any Node.js application running with --permission enabled that accepts attacker-controlled URLs or socket paths via net.connect, tls.connect, or undici/fetch with socketPath options.
Preconditions
The attacker needs the ability to supply a URL (e.g., SSRF) or socketPath option to a Node.js process running with the permission model enabled. No --allow-net flag is required for the attack to succeed.
Impact
SSRF in a web app running with --permission lets an attacker supply http://unix:/var/run/docker.sock:/containers/json to gain Docker control, spawn containers with host mounts, and achieve host RCE. Access to local secrets daemons (Vault agent sockets, database UDS listeners) is also possible without any --allow-net flag. The primary security guarantee of --permission is bypassed for UDS, invalidating the threat model for "no network" deployments.
Exploit path
How the issue forms.
PipeWrap::Connect calls uv_pipe_connect2 directly without any permission enforcement. Unlike TCP connections which explicitly check permissions, UDS connections bypass this entirely.
ConnectWrap* req_wrap =
new ConnectWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_PIPECONNECTWRAP);
int err = req_wrap->Dispatch(uv_pipe_connect2,
&wrap->handle_,
*name,
name.length(),
UV_PIPE_NO_TRUNCATE,
AfterConnect);For comparison, TCP connect enforces permissions using ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS. This check is absent from the UDS path in pipe_wrap.cc.
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kNet, ip_address.ToStringView(), args);Undici connector builds net.connect with the attacker-controlled socketPath, which uses PipeWrap internally and bypasses permission checks.
const options = { path: socketPath, ...opts }
// ...
socket = net.connect({
highWaterMark: 64 * 1024,
...options,
localAddress,
port,
host: hostname
})Permission model initialization sets up process.permission, but pipe_wrap IPC paths never call ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS, so the model is bypassed for all UDS operations.
// Permission model sets up kNet scope checks,
// but PipeWrap never queries themProof of concept
Reproduction.
Environment
Requirements: Build Node.js from source (./configure && make -j8) or use Node.js v25+ with the permission model.
Verify build:
./node --version
# v26.0.0-pre (or v25.x)
Configuration
Terminal 1: Start a UDS listener (benign stand-in for a privileged daemon)
./node -e "const net=require('net'); net.createServer(s=>{s.end('pong')}).listen('/tmp/perm.sock');"
Delivery
Terminal 2: Connect with permission model enabled but no --allow-net
Minimal Repro (net.connect to UDS):
./node --permission -e "const net=require('net'); const s=net.connect({path:'/tmp/perm.sock'},()=>{console.log('CONNECTED'); s.end();}); s.on('error',e=>console.error('ERR',e));"
Minimal Repro (undici/fetch over UDS):
./node --permission -e "const { request } = require('undici'); request('http://unix:/tmp/perm.sock:/').then(r=>r.body.text()).then(console.log).catch(console.error);"
Outcome
Both repros demonstrate that UDS connections bypass the permission model entirely. The net.connect example prints CONNECTED and the undici example successfully receives the response from the socket. This allows attackers to pivot through Node's runtime to access privileged local services (Docker API, database sockets, etc.) despite the operator enabling the permission model.
Remediation
Guidance.
Enforce permissions on UDS operations by adding ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS checks in src/pipe_wrap.cc for Connect and other UDS entrypoints. Treat the socket path as the resource string, using permission::PermissionScope::kNet to validate network access before allowing the connection.
Before
// src/pipe_wrap.cc:226 - No permission check before UDS connect
node::Utf8Value name(env->isolate(), args[1]);
ConnectWrap* req_wrap =
new ConnectWrap(env, req_wrap_obj, ...);
int err = req_wrap->Dispatch(uv_pipe_connect2, ...);After
// src/pipe_wrap.cc:226 - Permission check added after line 226
node::Utf8Value name(env->isolate(), args[1]);
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kNet, name.ToStringView(), args);
ConnectWrap* req_wrap =
new ConnectWrap(env, req_wrap_obj, ...);
int err = req_wrap->Dispatch(uv_pipe_connect2, ...);