Prototype Pollution
Description
The commit fixes a prototype pollution vulnerability in the HTTP incoming parser. It ensures that the destination objects used to accumulate distinct headers/trailers (headersDistinct and trailersDistinct) are created with a null prototype (via { __proto__: null }) instead of a plain {}. Previously, if a request included a __proto__ header, dest["__proto__"] could resolve to Object.prototype. Since Object.prototype is truthy and may have a push method on its prototype in certain workflows, the code could end up calling .push() on Object.prototype, leading to a TypeError and a crash. The fix prevents pollution by ensuring the destination objects cannot inherit from Object.prototype and thus cannot be polluted by a __proto__ header.
Proof of Concept
Proof-of-concept exploit (pre-patch): Start a vulnerable Node.js server (with the pre-fix code) and send an HTTP request containing a __proto__ header. The server may crash due to attempting to push into Object.prototype when collecting headers/trailers.
Example PoC using a raw TCP client (pre-patch scenario):
const net = require('net');
const port = process.argv[2] || 3000;
const client = net.connect(port, '127.0.0.1', () => {
client.write(
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'__proto__: test\r\n' +
'Connection: close\r\n' +
'\r\n'
);
});
client.on('end', () => console.log('done'));
Expected outcome (pre-patch): The server could crash with a TypeError similar to attempting to call .push() on Object.prototype due to the __proto__ header pollution.
Post-fix behavior (after this commit): The __proto__ header is stored in a null-prototype object (headersDistinct/trailersDistinct), and no pollution or crash occurs; req.headersDistinct will contain the '__proto__' key as a normal own property (e.g., { '__proto__': ['test'] }) without touching Object.prototype.
Commit Details
Author: Matteo Collina
Date: 2026-02-19 14:49 UTC
Message:
http: use null prototype for headersDistinct/trailersDistinct
Use { __proto__: null } instead of {} when initializing the
headersDistinct and trailersDistinct destination objects.
A plain {} inherits from Object.prototype, so when a __proto__
header is received, dest["__proto__"] resolves to Object.prototype
(truthy), causing _addHeaderLineDistinct to call .push() on it,
which throws an uncaught TypeError and crashes the process.
Ref: https://hackerone.com/reports/3560402
PR-URL: https://github.com/nodejs-private/node-private/pull/821
Refs: https://hackerone.com/reports/3560402
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
CVE-ID: CVE-2026-21710
Triage Assessment
Vulnerability Type: Prototype Pollution
Confidence: HIGH
Reasoning:
The change initializes destination header objects with a null prototype to prevent __proto__ header from affecting Object.prototype, which would cause pushes on Object.prototype and crash, enabling a prototype pollution-like vulnerability. The tests explicitly cover preventing pollution via __proto__ header and ensure headersDistinct/trailersDistinct have a null prototype.
Verification Assessment
Vulnerability Type: Prototype Pollution
Confidence: HIGH
Affected Versions: < 25.9.0
Code Diff
diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js
index 6cda3a84cee065..04b13358ca717f 100644
--- a/lib/_http_incoming.js
+++ b/lib/_http_incoming.js
@@ -128,7 +128,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
__proto__: null,
get: function() {
if (!this[kHeadersDistinct]) {
- this[kHeadersDistinct] = {};
+ this[kHeadersDistinct] = { __proto__: null };
const src = this.rawHeaders;
const dst = this[kHeadersDistinct];
@@ -168,7 +168,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
__proto__: null,
get: function() {
if (!this[kTrailersDistinct]) {
- this[kTrailersDistinct] = {};
+ this[kTrailersDistinct] = { __proto__: null };
const src = this.rawTrailers;
const dst = this[kTrailersDistinct];
diff --git a/test/parallel/test-http-headers-distinct-proto.js b/test/parallel/test-http-headers-distinct-proto.js
new file mode 100644
index 00000000000000..bd4cb82bd6e6b3
--- /dev/null
+++ b/test/parallel/test-http-headers-distinct-proto.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const net = require('net');
+
+// Regression test: sending a __proto__ header must not crash the server
+// when accessing req.headersDistinct or req.trailersDistinct.
+
+const server = http.createServer(common.mustCall((req, res) => {
+ const headers = req.headersDistinct;
+ assert.strictEqual(Object.getPrototypeOf(headers), null);
+ assert.deepStrictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, ['test']);
+ res.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+
+ const client = net.connect(port, common.mustCall(() => {
+ client.write(
+ 'GET / HTTP/1.1\r\n' +
+ 'Host: localhost\r\n' +
+ '__proto__: test\r\n' +
+ 'Connection: close\r\n' +
+ '\r\n',
+ );
+ }));
+
+ client.on('end', common.mustCall(() => {
+ server.close();
+ }));
+
+ client.resume();
+}));
diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js
index d01bca2fe2173c..75796e1faa9960 100644
--- a/test/parallel/test-http-multiple-headers.js
+++ b/test/parallel/test-http-multiple-headers.js
@@ -26,13 +26,13 @@ const server = createServer(
host,
'transfer-encoding': 'chunked'
});
- assert.deepStrictEqual(req.headersDistinct, {
+ assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, {
'connection': ['close'],
'x-req-a': ['eee', 'fff', 'ggg', 'hhh'],
'x-req-b': ['iii; jjj; kkk; lll'],
'host': [host],
- 'transfer-encoding': ['chunked']
- });
+ 'transfer-encoding': ['chunked'],
+ }));
req.on('end', common.mustCall(() => {
assert.deepStrictEqual(req.rawTrailers, [
@@ -45,7 +45,7 @@ const server = createServer(
);
assert.deepStrictEqual(
req.trailersDistinct,
- { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] }
+ Object.assign({ __proto__: null }, { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] })
);
res.setHeader('X-Res-a', 'AAA');
@@ -132,14 +132,14 @@ server.listen(0, common.mustCall(() => {
'x-res-d': 'JJJ; KKK; LLL',
'transfer-encoding': 'chunked'
});
- assert.deepStrictEqual(res.headersDistinct, {
+ assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, {
'x-res-a': [ 'AAA', 'BBB', 'CCC' ],
'x-res-b': [ 'DDD; EEE; FFF; GGG' ],
'connection': [ 'close' ],
'x-res-c': [ 'HHH', 'III' ],
'x-res-d': [ 'JJJ; KKK; LLL' ],
- 'transfer-encoding': [ 'chunked' ]
- });
+ 'transfer-encoding': [ 'chunked' ],
+ }));
res.on('end', common.mustCall(() => {
assert.deepStrictEqual(res.rawTrailers, [
@@ -153,7 +153,7 @@ server.listen(0, common.mustCall(() => {
);
assert.deepStrictEqual(
res.trailersDistinct,
- { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] }
+ Object.assign({ __proto__: null }, { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] })
);
server.close();
}));