http: use null prototype for headersDistinct/trailersDistinct
authorMatteo Collina <hello@matteocollina.com>
Thu, 19 Feb 2026 14:49:43 +0000 (15:49 +0100)
committerBastien Roucariès <rouca@debian.org>
Mon, 6 Apr 2026 14:18:52 +0000 (16:18 +0200)
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
origin: https://github.com/nodejs/node/commit/00ad47a28eb2e3dc0ff5610d58c53341acf3cf8d

Gbp-Pq: Name CVE-2026-21710.patch

lib/_http_incoming.js
test/parallel/test-http-headers-distinct-proto.js [new file with mode: 0644]
test/parallel/test-http-multiple-headers.js

index e45ae8190e22150aaedd861cc83baeed65af0965..77433e55766635704036d2e633b62adadd9e7241 100644 (file)
@@ -131,7 +131,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];
@@ -171,7 +171,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 (file)
index 0000000..bd4cb82
--- /dev/null
@@ -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();
+}));
index 1ebd290a614b1b1c55d14f221788596faf485f26..1188af5d22fa25b2307971c2a3463bdfc7c1d6b8 100644 (file)
@@ -27,13 +27,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', function() {
       assert.deepStrictEqual(req.rawTrailers, [
@@ -46,7 +46,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');
@@ -129,14 +129,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', function() {
       assert.deepStrictEqual(res.rawTrailers, [
@@ -150,7 +150,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();
     });