From 0a071d8a9d52ff1d7795fbca5e51915960a9d013 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 21 Apr 2026 10:44:34 +0000 Subject: [PATCH 1/3] http: make req.headers have a null prototype Makes IncomingMessage.prototype.headers and trailers have a null prototype, matching the existing behavior of headersDistinct and trailersDistinct. Fixes prototype pollution concerns where headers like __proto__ could be interpreted as prototype manipulation. Refs: https://github.com/nodejs/node/issues/61771 PR-URL: https://github.com/nodejs/node/pull/61772 --- lib/_http_incoming.js | 4 +- test/parallel/test-http-blank-header.js | 2 +- .../test-http-client-headers-array.js | 2 +- test/parallel/test-http-content-length.js | 12 +-- test/parallel/test-http-multiple-headers.js | 8 +- test/parallel/test-http-raw-headers.js | 8 +- .../test-http-remove-header-stays-removed.js | 2 +- .../test-http-server-headers-null-proto.js | 95 +++++++++++++++++++ test/parallel/test-http-upgrade-agent.js | 2 +- test/parallel/test-http-upgrade-client.js | 3 +- ...tp-upgrade-server-with-body-and-extras.mjs | 2 +- 11 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 test/parallel/test-http-server-headers-null-proto.js diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 18aed3b8df037d..6c082454b08a2b 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -112,7 +112,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headers', { __proto__: null, get: function() { if (!this[kHeaders]) { - this[kHeaders] = {}; + this[kHeaders] = { __proto__: null }; const src = this.rawHeaders; const dst = this[kHeaders]; @@ -152,7 +152,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailers', { __proto__: null, get: function() { if (!this[kTrailers]) { - this[kTrailers] = {}; + this[kTrailers] = { __proto__: null }; const src = this.rawTrailers; const dst = this[kTrailers]; diff --git a/test/parallel/test-http-blank-header.js b/test/parallel/test-http-blank-header.js index 696b16f4995b57..c3e2ec95cf1b69 100644 --- a/test/parallel/test-http-blank-header.js +++ b/test/parallel/test-http-blank-header.js @@ -28,7 +28,7 @@ const net = require('net'); const server = http.createServer(common.mustCall((req, res) => { assert.strictEqual(req.method, 'GET'); assert.strictEqual(req.url, '/blah'); - assert.deepStrictEqual(req.headers, { + assert.deepStrictEqual(req.headers, { __proto__: null, host: 'example.org:443', origin: 'http://example.org', cookie: '' diff --git a/test/parallel/test-http-client-headers-array.js b/test/parallel/test-http-client-headers-array.js index f5a9430b3261f2..38f027c2718ef1 100644 --- a/test/parallel/test-http-client-headers-array.js +++ b/test/parallel/test-http-client-headers-array.js @@ -7,7 +7,7 @@ const http = require('http'); function execute(options) { http.createServer(common.mustCall(function(req, res) { - const expectHeaders = { + const expectHeaders = { __proto__: null, 'x-foo': 'boom', 'cookie': 'a=1; b=2; c=3', 'connection': 'keep-alive', diff --git a/test/parallel/test-http-content-length.js b/test/parallel/test-http-content-length.js index e731b0e8ef92aa..780262b69921c6 100644 --- a/test/parallel/test-http-content-length.js +++ b/test/parallel/test-http-content-length.js @@ -4,17 +4,17 @@ const assert = require('assert'); const http = require('http'); const Countdown = require('../common/countdown'); -const expectedHeadersMultipleWrites = { +const expectedHeadersMultipleWrites = { __proto__: null, 'connection': 'keep-alive', 'transfer-encoding': 'chunked', }; -const expectedHeadersEndWithData = { +const expectedHeadersEndWithData = { __proto__: null, 'connection': 'keep-alive', 'content-length': String('hello world'.length), }; -const expectedHeadersEndNoData = { +const expectedHeadersEndNoData = { __proto__: null, 'connection': 'keep-alive', 'content-length': '0', }; @@ -62,7 +62,7 @@ server.listen(0, common.mustCall(function() { req.write('hello '); req.end('world'); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' }); res.resume(); })); @@ -74,7 +74,7 @@ server.listen(0, common.mustCall(function() { req.removeHeader('Date'); req.end('hello world'); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' }); res.resume(); })); @@ -86,7 +86,7 @@ server.listen(0, common.mustCall(function() { req.removeHeader('Date'); req.end(); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' }); res.resume(); })); diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js index 75796e1faa9960..eb4adc3038d867 100644 --- a/test/parallel/test-http-multiple-headers.js +++ b/test/parallel/test-http-multiple-headers.js @@ -19,7 +19,7 @@ const server = createServer( 'Host', host, 'Transfer-Encoding', 'chunked', ]); - assert.deepStrictEqual(req.headers, { + assert.deepStrictEqual(req.headers, { __proto__: null, 'connection': 'close', 'x-req-a': 'eee, fff, ggg, hhh', 'x-req-b': 'iii; jjj; kkk; lll', @@ -41,7 +41,7 @@ const server = createServer( 'X-req-Y', 'zzz; www', ]); assert.deepStrictEqual( - req.trailers, { 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' } + req.trailers, { __proto__: null, 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' } ); assert.deepStrictEqual( req.trailersDistinct, @@ -124,7 +124,7 @@ server.listen(0, common.mustCall(() => { 'x-res-d', 'JJJ; KKK; LLL', 'Transfer-Encoding', 'chunked', ]); - assert.deepStrictEqual(res.headers, { + assert.deepStrictEqual(res.headers, { __proto__: null, 'x-res-a': 'AAA, BBB, CCC', 'x-res-b': 'DDD; EEE; FFF; GGG', 'connection': 'close', @@ -149,7 +149,7 @@ server.listen(0, common.mustCall(() => { ]); assert.deepStrictEqual( res.trailers, - { 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' } + { __proto__: null, 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' } ); assert.deepStrictEqual( res.trailersDistinct, diff --git a/test/parallel/test-http-raw-headers.js b/test/parallel/test-http-raw-headers.js index c6ca5f5c7a2a5a..535e20bc0b737b 100644 --- a/test/parallel/test-http-raw-headers.js +++ b/test/parallel/test-http-raw-headers.js @@ -36,7 +36,7 @@ http.createServer(common.mustCall(function(req, res) { 'Connection', 'keep-alive', ]; - const expectHeaders = { + const expectHeaders = { __proto__: null, 'host': `localhost:${this.address().port}`, 'transfer-encoding': 'CHUNKED', 'x-bar': 'yoyoyo', @@ -52,7 +52,7 @@ http.createServer(common.mustCall(function(req, res) { 'X-baR', 'OyOyOyO', ]; - const expectTrailers = { 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' }; + const expectTrailers = { __proto__: null, 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' }; this.close(); @@ -98,7 +98,7 @@ http.createServer(common.mustCall(function(req, res) { 'Transfer-Encoding', 'chunked', ]; - const expectHeaders = { + const expectHeaders = { __proto__: null, 'keep-alive': 'timeout=1', 'trailer': 'x-foo', 'date': null, @@ -120,7 +120,7 @@ http.createServer(common.mustCall(function(req, res) { 'X-foO', 'OxOxOxO', ]; - const expectTrailers = { 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' }; + const expectTrailers = { __proto__: null, 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' }; assert.deepStrictEqual(res.rawTrailers, expectRawTrailers); assert.deepStrictEqual(res.trailers, expectTrailers); diff --git a/test/parallel/test-http-remove-header-stays-removed.js b/test/parallel/test-http-remove-header-stays-removed.js index 301a5f8d8e4cd4..f39fd2480db571 100644 --- a/test/parallel/test-http-remove-header-stays-removed.js +++ b/test/parallel/test-http-remove-header-stays-removed.js @@ -50,7 +50,7 @@ const server = http.createServer(common.mustCall(function(request, response) { server.listen(0, common.mustCall(function() { http.get({ port: this.address().port }, common.mustCall((res) => { assert.strictEqual(res.statusCode, 200); - assert.deepStrictEqual(res.headers, { date: 'coffee o clock' }); + assert.deepStrictEqual(res.headers, { __proto__: null, date: 'coffee o clock' }); let response = ''; res.setEncoding('ascii'); diff --git a/test/parallel/test-http-server-headers-null-proto.js b/test/parallel/test-http-server-headers-null-proto.js new file mode 100644 index 00000000000000..ded2c334eed657 --- /dev/null +++ b/test/parallel/test-http-server-headers-null-proto.js @@ -0,0 +1,95 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const net = require('net'); + +// Test 1: req.headers has a null prototype +{ + const server = http.createServer(common.mustCall((req, res) => { + assert.strictEqual(Object.getPrototypeOf(req.headers), null); + 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' + + 'Connection: close\r\n' + + '\r\n', + ); + })); + + client.on('end', common.mustCall(() => { + server.close(); + })); + + client.resume(); + })); +} + +// Test 2: req.trailers has a null prototype +{ + const server = http.createServer(common.mustCall((req, res) => { + res.setHeader('Transfer-Encoding', 'chunked'); + res.write('Hello'); + res.addTrailers({ + 'X-Trailer': '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' + + 'TE: trailers\r\n' + + 'Connection: close\r\n' + + '\r\n', + ); + })); + + client.on('data', () => {}); + + client.on('end', common.mustCall(() => { + server.close(); + })); + })); +} + +// Test 3: req.headers with __proto__ header (should not pollute prototype) +{ + const server = http.createServer(common.mustCall((req, res) => { + assert.strictEqual(Object.getPrototypeOf(req.headers), null); + // The __proto__ header should be stored as a regular property + assert.strictEqual(req.headers['__proto__'], '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-upgrade-agent.js b/test/parallel/test-http-upgrade-agent.js index d2969522433db1..1b96a841474e69 100644 --- a/test/parallel/test-http-upgrade-agent.js +++ b/test/parallel/test-http-upgrade-agent.js @@ -76,7 +76,7 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { assert.strictEqual(recvData.toString(), 'nurtzo'); })); - const expectedHeaders = { 'hello': 'world', + const expectedHeaders = { __proto__: null, 'hello': 'world', 'connection': 'upgrade', 'upgrade': 'websocket' }; assert.deepStrictEqual(expectedHeaders, res.headers); diff --git a/test/parallel/test-http-upgrade-client.js b/test/parallel/test-http-upgrade-client.js index fbeaf08c2c5582..caa0d4baa41ed5 100644 --- a/test/parallel/test-http-upgrade-client.js +++ b/test/parallel/test-http-upgrade-client.js @@ -85,7 +85,8 @@ server.listen(0, common.mustCall(function() { const expectedHeaders = { hello: 'world', connection: 'upgrade', - upgrade: 'websocket' + upgrade: 'websocket', + __proto__: null }; assert.deepStrictEqual(res.headers, expectedHeaders); socket.end(); diff --git a/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs b/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs index 32de17ab7a57a7..1b877805e6023f 100644 --- a/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs +++ b/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs @@ -24,7 +24,7 @@ server.on('upgrade', common.mustCall(function(req, socket, upgradeHead) { req.on('end', common.mustCall(() => { assert.strictEqual(reqBodyLength, EXPECTED_BODY_LENGTH); - assert.deepStrictEqual(req.trailers, { 'extra-data': 'abc' }); + assert.deepStrictEqual(req.trailers, { __proto__: null, 'extra-data': 'abc' }); // Defer upgrade stream read slightly to make sure it doesn't start // streaming along with the request body, until we actually read it: From 8c7c0407cee60e2a9f3fc78d53a385c12beb5235 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 22 Apr 2026 06:15:02 +0000 Subject: [PATCH 2/3] http2/compat: make req.trailers have a null prototype In HTTP/2 compatibility mode, make request trailers have a null prototype, matching the behavior of regular headers and trailers. --- lib/internal/http2/compat.js | 2 +- test/parallel/test-http-blank-header.js | 7 ++-- .../test-http-client-headers-array.js | 11 +++---- test/parallel/test-http-content-length.js | 31 +++++++++--------- test/parallel/test-http-multiple-headers.js | 32 +++++++++---------- test/parallel/test-http-raw-headers.js | 28 ++++++++-------- .../test-http-server-headers-null-proto.js | 4 ++- test/parallel/test-http-upgrade-agent.js | 2 +- ...tp-upgrade-server-with-body-and-extras.mjs | 2 +- ...est-http2-compat-serverrequest-trailers.js | 1 + 10 files changed, 59 insertions(+), 61 deletions(-) diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index d1728278713fad..41909ffa5756f8 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -319,7 +319,7 @@ class Http2ServerRequest extends Readable { // initialization using Object.create(null) in HTTP/2 is intentional. this[kHeaders] = headers; this[kRawHeaders] = rawHeaders; - this[kTrailers] = {}; + this[kTrailers] = { __proto__: null }; this[kRawTrailers] = []; this[kStream] = stream; this[kAborted] = false; diff --git a/test/parallel/test-http-blank-header.js b/test/parallel/test-http-blank-header.js index c3e2ec95cf1b69..a22f2f32ffcfa3 100644 --- a/test/parallel/test-http-blank-header.js +++ b/test/parallel/test-http-blank-header.js @@ -29,10 +29,9 @@ const server = http.createServer(common.mustCall((req, res) => { assert.strictEqual(req.method, 'GET'); assert.strictEqual(req.url, '/blah'); assert.deepStrictEqual(req.headers, { __proto__: null, - host: 'example.org:443', - origin: 'http://example.org', - cookie: '' - }); + host: 'example.org:443', + origin: 'http://example.org', + cookie: '' }); })); diff --git a/test/parallel/test-http-client-headers-array.js b/test/parallel/test-http-client-headers-array.js index 38f027c2718ef1..697bc0b4600fa2 100644 --- a/test/parallel/test-http-client-headers-array.js +++ b/test/parallel/test-http-client-headers-array.js @@ -7,12 +7,11 @@ const http = require('http'); function execute(options) { http.createServer(common.mustCall(function(req, res) { - const expectHeaders = { __proto__: null, - 'x-foo': 'boom', - 'cookie': 'a=1; b=2; c=3', - 'connection': 'keep-alive', - 'host': 'example.com', - }; + const expectHeaders = { '__proto__': null, + 'x-foo': 'boom', + 'cookie': 'a=1; b=2; c=3', + 'connection': 'keep-alive', + 'host': 'example.com' }; // no Host header when you set headers an array if (!Array.isArray(options.headers)) { diff --git a/test/parallel/test-http-content-length.js b/test/parallel/test-http-content-length.js index 780262b69921c6..69fa6ae5f657e0 100644 --- a/test/parallel/test-http-content-length.js +++ b/test/parallel/test-http-content-length.js @@ -4,20 +4,17 @@ const assert = require('assert'); const http = require('http'); const Countdown = require('../common/countdown'); -const expectedHeadersMultipleWrites = { __proto__: null, - 'connection': 'keep-alive', - 'transfer-encoding': 'chunked', -}; +const expectedHeadersMultipleWrites = { '__proto__': null, + 'connection': 'keep-alive', + 'transfer-encoding': 'chunked' }; -const expectedHeadersEndWithData = { __proto__: null, - 'connection': 'keep-alive', - 'content-length': String('hello world'.length), -}; +const expectedHeadersEndWithData = { '__proto__': null, + 'connection': 'keep-alive', + 'content-length': String('hello world'.length) }; -const expectedHeadersEndNoData = { __proto__: null, - 'connection': 'keep-alive', - 'content-length': '0', -}; +const expectedHeadersEndNoData = { '__proto__': null, + 'connection': 'keep-alive', + 'content-length': '0' }; const countdown = new Countdown(3, () => server.close()); @@ -62,7 +59,9 @@ server.listen(0, common.mustCall(function() { req.write('hello '); req.end('world'); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { + '__proto__': null, ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1', + }); res.resume(); })); @@ -74,7 +73,9 @@ server.listen(0, common.mustCall(function() { req.removeHeader('Date'); req.end('hello world'); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { + '__proto__': null, ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1', + }); res.resume(); })); @@ -86,7 +87,7 @@ server.listen(0, common.mustCall(function() { req.removeHeader('Date'); req.end(); req.on('response', common.mustCall((res) => { - assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' }); + assert.deepStrictEqual(res.headers, { '__proto__': null, ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' }); res.resume(); })); diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js index eb4adc3038d867..f0dbd5942a9299 100644 --- a/test/parallel/test-http-multiple-headers.js +++ b/test/parallel/test-http-multiple-headers.js @@ -19,13 +19,12 @@ const server = createServer( 'Host', host, 'Transfer-Encoding', 'chunked', ]); - assert.deepStrictEqual(req.headers, { __proto__: null, - 'connection': 'close', - 'x-req-a': 'eee, fff, ggg, hhh', - 'x-req-b': 'iii; jjj; kkk; lll', - host, - 'transfer-encoding': 'chunked' - }); + assert.deepStrictEqual(req.headers, { '__proto__': null, + 'connection': 'close', + 'x-req-a': 'eee, fff, ggg, hhh', + 'x-req-b': 'iii; jjj; kkk; lll', + host, + 'transfer-encoding': 'chunked' }); assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, { 'connection': ['close'], 'x-req-a': ['eee', 'fff', 'ggg', 'hhh'], @@ -41,7 +40,7 @@ const server = createServer( 'X-req-Y', 'zzz; www', ]); assert.deepStrictEqual( - req.trailers, { __proto__: null, 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' } + req.trailers, { '__proto__': null, 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' } ); assert.deepStrictEqual( req.trailersDistinct, @@ -124,14 +123,13 @@ server.listen(0, common.mustCall(() => { 'x-res-d', 'JJJ; KKK; LLL', 'Transfer-Encoding', 'chunked', ]); - assert.deepStrictEqual(res.headers, { __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' - }); + assert.deepStrictEqual(res.headers, { '__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' }); assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, { 'x-res-a': [ 'AAA', 'BBB', 'CCC' ], 'x-res-b': [ 'DDD; EEE; FFF; GGG' ], @@ -149,7 +147,7 @@ server.listen(0, common.mustCall(() => { ]); assert.deepStrictEqual( res.trailers, - { __proto__: null, 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' } + { '__proto__': null, 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' } ); assert.deepStrictEqual( res.trailersDistinct, diff --git a/test/parallel/test-http-raw-headers.js b/test/parallel/test-http-raw-headers.js index 535e20bc0b737b..e03dc4445f18cc 100644 --- a/test/parallel/test-http-raw-headers.js +++ b/test/parallel/test-http-raw-headers.js @@ -36,12 +36,11 @@ http.createServer(common.mustCall(function(req, res) { 'Connection', 'keep-alive', ]; - const expectHeaders = { __proto__: null, - 'host': `localhost:${this.address().port}`, - 'transfer-encoding': 'CHUNKED', - 'x-bar': 'yoyoyo', - 'connection': 'keep-alive' - }; + const expectHeaders = { '__proto__': null, + 'host': `localhost:${this.address().port}`, + 'transfer-encoding': 'CHUNKED', + 'x-bar': 'yoyoyo', + 'connection': 'keep-alive' }; const expectRawTrailers = [ 'x-bAr', 'yOyOyOy', @@ -52,7 +51,7 @@ http.createServer(common.mustCall(function(req, res) { 'X-baR', 'OyOyOyO', ]; - const expectTrailers = { __proto__: null, 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' }; + const expectTrailers = { '__proto__': null, 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' }; this.close(); @@ -98,13 +97,12 @@ http.createServer(common.mustCall(function(req, res) { 'Transfer-Encoding', 'chunked', ]; - const expectHeaders = { __proto__: null, - 'keep-alive': 'timeout=1', - 'trailer': 'x-foo', - 'date': null, - 'connection': 'keep-alive', - 'transfer-encoding': 'chunked' - }; + const expectHeaders = { '__proto__': null, + 'keep-alive': 'timeout=1', + 'trailer': 'x-foo', + 'date': null, + 'connection': 'keep-alive', + 'transfer-encoding': 'chunked' }; res.rawHeaders[5] = null; res.headers.date = null; assert.deepStrictEqual(res.rawHeaders, expectRawHeaders); @@ -120,7 +118,7 @@ http.createServer(common.mustCall(function(req, res) { 'X-foO', 'OxOxOxO', ]; - const expectTrailers = { __proto__: null, 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' }; + const expectTrailers = { '__proto__': null, 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' }; assert.deepStrictEqual(res.rawTrailers, expectRawTrailers); assert.deepStrictEqual(res.trailers, expectTrailers); diff --git a/test/parallel/test-http-server-headers-null-proto.js b/test/parallel/test-http-server-headers-null-proto.js index ded2c334eed657..cb97753f4960bb 100644 --- a/test/parallel/test-http-server-headers-null-proto.js +++ b/test/parallel/test-http-server-headers-null-proto.js @@ -69,7 +69,9 @@ const net = require('net'); const server = http.createServer(common.mustCall((req, res) => { assert.strictEqual(Object.getPrototypeOf(req.headers), null); // The __proto__ header should be stored as a regular property - assert.strictEqual(req.headers['__proto__'], 'test'); + assert.strictEqual( + Object.getOwnPropertyDescriptor(req.headers, '__proto__').value, 'test', + ); res.end(); })); diff --git a/test/parallel/test-http-upgrade-agent.js b/test/parallel/test-http-upgrade-agent.js index 1b96a841474e69..3ec774a2a795b4 100644 --- a/test/parallel/test-http-upgrade-agent.js +++ b/test/parallel/test-http-upgrade-agent.js @@ -76,7 +76,7 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { assert.strictEqual(recvData.toString(), 'nurtzo'); })); - const expectedHeaders = { __proto__: null, 'hello': 'world', + const expectedHeaders = { '__proto__': null, 'hello': 'world', 'connection': 'upgrade', 'upgrade': 'websocket' }; assert.deepStrictEqual(expectedHeaders, res.headers); diff --git a/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs b/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs index 1b877805e6023f..bcd24faadb8054 100644 --- a/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs +++ b/test/parallel/test-http-upgrade-server-with-body-and-extras.mjs @@ -24,7 +24,7 @@ server.on('upgrade', common.mustCall(function(req, socket, upgradeHead) { req.on('end', common.mustCall(() => { assert.strictEqual(reqBodyLength, EXPECTED_BODY_LENGTH); - assert.deepStrictEqual(req.trailers, { __proto__: null, 'extra-data': 'abc' }); + assert.deepStrictEqual(req.trailers, { '__proto__': null, 'extra-data': 'abc' }); // Defer upgrade stream read slightly to make sure it doesn't start // streaming along with the request body, until we actually read it: diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js index 620ae69029a375..bfa06231b8c9d7 100644 --- a/test/parallel/test-http2-compat-serverrequest-trailers.js +++ b/test/parallel/test-http2-compat-serverrequest-trailers.js @@ -22,6 +22,7 @@ server.listen(0, common.mustCall(function() { request.on('data', common.mustCallAtLeast((chunk) => data += chunk)); request.on('end', common.mustCall(() => { const trailers = request.trailers; + assert.strictEqual(Object.getPrototypeOf(trailers), null); for (const [name, value] of Object.entries(expectedTrailers)) { assert.strictEqual(trailers[name], value); } From 3a57b11e2fd3ad3e7f4a8eddf88b1b56ee48eb2a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 23 Apr 2026 15:27:08 +0000 Subject: [PATCH 3/3] test: actually check req.trailers null prototype in http-server-headers-null-proto --- doc/api/http.md | 12 ++++++++++++ doc/api/http2.md | 6 ++++++ test/parallel/test-http-server-headers-null-proto.js | 11 ++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/doc/api/http.md b/doc/api/http.md index 8ac1ed166103b0..31e4760083677d 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2863,6 +2863,9 @@ The request/response headers object. Key-value pairs of header names and values. Header names are lower-cased. +The object has a null prototype and should not be accessed using the `in` +operator. + ```js // Prints something like: // @@ -2900,6 +2903,9 @@ added: Similar to [`message.headers`][], but there is no join logic and the values are always arrays of strings, even for headers received just once. +The object has a null prototype and should not be accessed using the `in` +operator. + ```js // Prints something like: // @@ -3086,6 +3092,9 @@ added: v0.3.0 The request/response trailers object. Only populated at the `'end'` event. +The object has a null prototype and should not be accessed using the `in` +operator. + ### `message.trailersDistinct`