Skip to content

Commit 0a071d8

Browse files
committed
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: #61771 PR-URL: #61772
1 parent cee146f commit 0a071d8

11 files changed

Lines changed: 118 additions & 22 deletions

lib/_http_incoming.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
112112
__proto__: null,
113113
get: function() {
114114
if (!this[kHeaders]) {
115-
this[kHeaders] = {};
115+
this[kHeaders] = { __proto__: null };
116116

117117
const src = this.rawHeaders;
118118
const dst = this[kHeaders];
@@ -152,7 +152,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
152152
__proto__: null,
153153
get: function() {
154154
if (!this[kTrailers]) {
155-
this[kTrailers] = {};
155+
this[kTrailers] = { __proto__: null };
156156

157157
const src = this.rawTrailers;
158158
const dst = this[kTrailers];

test/parallel/test-http-blank-header.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const net = require('net');
2828
const server = http.createServer(common.mustCall((req, res) => {
2929
assert.strictEqual(req.method, 'GET');
3030
assert.strictEqual(req.url, '/blah');
31-
assert.deepStrictEqual(req.headers, {
31+
assert.deepStrictEqual(req.headers, { __proto__: null,
3232
host: 'example.org:443',
3333
origin: 'http://example.org',
3434
cookie: ''

test/parallel/test-http-client-headers-array.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const http = require('http');
77

88
function execute(options) {
99
http.createServer(common.mustCall(function(req, res) {
10-
const expectHeaders = {
10+
const expectHeaders = { __proto__: null,
1111
'x-foo': 'boom',
1212
'cookie': 'a=1; b=2; c=3',
1313
'connection': 'keep-alive',

test/parallel/test-http-content-length.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ const assert = require('assert');
44
const http = require('http');
55
const Countdown = require('../common/countdown');
66

7-
const expectedHeadersMultipleWrites = {
7+
const expectedHeadersMultipleWrites = { __proto__: null,
88
'connection': 'keep-alive',
99
'transfer-encoding': 'chunked',
1010
};
1111

12-
const expectedHeadersEndWithData = {
12+
const expectedHeadersEndWithData = { __proto__: null,
1313
'connection': 'keep-alive',
1414
'content-length': String('hello world'.length),
1515
};
1616

17-
const expectedHeadersEndNoData = {
17+
const expectedHeadersEndNoData = { __proto__: null,
1818
'connection': 'keep-alive',
1919
'content-length': '0',
2020
};
@@ -62,7 +62,7 @@ server.listen(0, common.mustCall(function() {
6262
req.write('hello ');
6363
req.end('world');
6464
req.on('response', common.mustCall((res) => {
65-
assert.deepStrictEqual(res.headers, { ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' });
65+
assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' });
6666
res.resume();
6767
}));
6868

@@ -74,7 +74,7 @@ server.listen(0, common.mustCall(function() {
7474
req.removeHeader('Date');
7575
req.end('hello world');
7676
req.on('response', common.mustCall((res) => {
77-
assert.deepStrictEqual(res.headers, { ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' });
77+
assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' });
7878
res.resume();
7979
}));
8080

@@ -86,7 +86,7 @@ server.listen(0, common.mustCall(function() {
8686
req.removeHeader('Date');
8787
req.end();
8888
req.on('response', common.mustCall((res) => {
89-
assert.deepStrictEqual(res.headers, { ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' });
89+
assert.deepStrictEqual(res.headers, { __proto__: null, ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' });
9090
res.resume();
9191
}));
9292

test/parallel/test-http-multiple-headers.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const server = createServer(
1919
'Host', host,
2020
'Transfer-Encoding', 'chunked',
2121
]);
22-
assert.deepStrictEqual(req.headers, {
22+
assert.deepStrictEqual(req.headers, { __proto__: null,
2323
'connection': 'close',
2424
'x-req-a': 'eee, fff, ggg, hhh',
2525
'x-req-b': 'iii; jjj; kkk; lll',
@@ -41,7 +41,7 @@ const server = createServer(
4141
'X-req-Y', 'zzz; www',
4242
]);
4343
assert.deepStrictEqual(
44-
req.trailers, { 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' }
44+
req.trailers, { __proto__: null, 'x-req-x': 'xxx, yyy', 'x-req-y': 'zzz; www' }
4545
);
4646
assert.deepStrictEqual(
4747
req.trailersDistinct,
@@ -124,7 +124,7 @@ server.listen(0, common.mustCall(() => {
124124
'x-res-d', 'JJJ; KKK; LLL',
125125
'Transfer-Encoding', 'chunked',
126126
]);
127-
assert.deepStrictEqual(res.headers, {
127+
assert.deepStrictEqual(res.headers, { __proto__: null,
128128
'x-res-a': 'AAA, BBB, CCC',
129129
'x-res-b': 'DDD; EEE; FFF; GGG',
130130
'connection': 'close',
@@ -149,7 +149,7 @@ server.listen(0, common.mustCall(() => {
149149
]);
150150
assert.deepStrictEqual(
151151
res.trailers,
152-
{ 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' }
152+
{ __proto__: null, 'x-res-x': 'XXX, YYY', 'x-res-y': 'ZZZ; WWW' }
153153
);
154154
assert.deepStrictEqual(
155155
res.trailersDistinct,

test/parallel/test-http-raw-headers.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ http.createServer(common.mustCall(function(req, res) {
3636
'Connection',
3737
'keep-alive',
3838
];
39-
const expectHeaders = {
39+
const expectHeaders = { __proto__: null,
4040
'host': `localhost:${this.address().port}`,
4141
'transfer-encoding': 'CHUNKED',
4242
'x-bar': 'yoyoyo',
@@ -52,7 +52,7 @@ http.createServer(common.mustCall(function(req, res) {
5252
'X-baR',
5353
'OyOyOyO',
5454
];
55-
const expectTrailers = { 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' };
55+
const expectTrailers = { __proto__: null, 'x-bar': 'yOyOyOy, OyOyOyO, yOyOyOy, OyOyOyO' };
5656

5757
this.close();
5858

@@ -98,7 +98,7 @@ http.createServer(common.mustCall(function(req, res) {
9898
'Transfer-Encoding',
9999
'chunked',
100100
];
101-
const expectHeaders = {
101+
const expectHeaders = { __proto__: null,
102102
'keep-alive': 'timeout=1',
103103
'trailer': 'x-foo',
104104
'date': null,
@@ -120,7 +120,7 @@ http.createServer(common.mustCall(function(req, res) {
120120
'X-foO',
121121
'OxOxOxO',
122122
];
123-
const expectTrailers = { 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' };
123+
const expectTrailers = { __proto__: null, 'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO' };
124124

125125
assert.deepStrictEqual(res.rawTrailers, expectRawTrailers);
126126
assert.deepStrictEqual(res.trailers, expectTrailers);

test/parallel/test-http-remove-header-stays-removed.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const server = http.createServer(common.mustCall(function(request, response) {
5050
server.listen(0, common.mustCall(function() {
5151
http.get({ port: this.address().port }, common.mustCall((res) => {
5252
assert.strictEqual(res.statusCode, 200);
53-
assert.deepStrictEqual(res.headers, { date: 'coffee o clock' });
53+
assert.deepStrictEqual(res.headers, { __proto__: null, date: 'coffee o clock' });
5454

5555
let response = '';
5656
res.setEncoding('ascii');
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const net = require('net');
7+
8+
// Test 1: req.headers has a null prototype
9+
{
10+
const server = http.createServer(common.mustCall((req, res) => {
11+
assert.strictEqual(Object.getPrototypeOf(req.headers), null);
12+
res.end();
13+
}));
14+
15+
server.listen(0, common.mustCall(() => {
16+
const port = server.address().port;
17+
18+
const client = net.connect(port, common.mustCall(() => {
19+
client.write(
20+
'GET / HTTP/1.1\r\n' +
21+
'Host: localhost\r\n' +
22+
'Connection: close\r\n' +
23+
'\r\n',
24+
);
25+
}));
26+
27+
client.on('end', common.mustCall(() => {
28+
server.close();
29+
}));
30+
31+
client.resume();
32+
}));
33+
}
34+
35+
// Test 2: req.trailers has a null prototype
36+
{
37+
const server = http.createServer(common.mustCall((req, res) => {
38+
res.setHeader('Transfer-Encoding', 'chunked');
39+
res.write('Hello');
40+
res.addTrailers({
41+
'X-Trailer': 'test',
42+
});
43+
res.end();
44+
}));
45+
46+
server.listen(0, common.mustCall(() => {
47+
const port = server.address().port;
48+
49+
const client = net.connect(port, common.mustCall(() => {
50+
client.write(
51+
'GET / HTTP/1.1\r\n' +
52+
'Host: localhost\r\n' +
53+
'TE: trailers\r\n' +
54+
'Connection: close\r\n' +
55+
'\r\n',
56+
);
57+
}));
58+
59+
client.on('data', () => {});
60+
61+
client.on('end', common.mustCall(() => {
62+
server.close();
63+
}));
64+
}));
65+
}
66+
67+
// Test 3: req.headers with __proto__ header (should not pollute prototype)
68+
{
69+
const server = http.createServer(common.mustCall((req, res) => {
70+
assert.strictEqual(Object.getPrototypeOf(req.headers), null);
71+
// The __proto__ header should be stored as a regular property
72+
assert.strictEqual(req.headers['__proto__'], 'test');
73+
res.end();
74+
}));
75+
76+
server.listen(0, common.mustCall(() => {
77+
const port = server.address().port;
78+
79+
const client = net.connect(port, common.mustCall(() => {
80+
client.write(
81+
'GET / HTTP/1.1\r\n' +
82+
'Host: localhost\r\n' +
83+
'__proto__: test\r\n' +
84+
'Connection: close\r\n' +
85+
'\r\n',
86+
);
87+
}));
88+
89+
client.on('end', common.mustCall(() => {
90+
server.close();
91+
}));
92+
93+
client.resume();
94+
}));
95+
}

test/parallel/test-http-upgrade-agent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ server.listen(0, '127.0.0.1', common.mustCall(function() {
7676
assert.strictEqual(recvData.toString(), 'nurtzo');
7777
}));
7878

79-
const expectedHeaders = { 'hello': 'world',
79+
const expectedHeaders = { __proto__: null, 'hello': 'world',
8080
'connection': 'upgrade',
8181
'upgrade': 'websocket' };
8282
assert.deepStrictEqual(expectedHeaders, res.headers);

test/parallel/test-http-upgrade-client.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ server.listen(0, common.mustCall(function() {
8585
const expectedHeaders = {
8686
hello: 'world',
8787
connection: 'upgrade',
88-
upgrade: 'websocket'
88+
upgrade: 'websocket',
89+
__proto__: null
8990
};
9091
assert.deepStrictEqual(res.headers, expectedHeaders);
9192
socket.end();

0 commit comments

Comments
 (0)