Prototype Pollution

HIGH
nodejs/node
Commit: ef5929b5aaae
Affected: < 25.9.0
2026-04-05 10:15 UTC

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(); }));
← Back to Alerts View on GitHub →