Skip to content

Commit c8525ac

Browse files
mcollinaRafaelGSS
authored andcommitted
http: reject requests exceeding max header count
(cherry picked from commit 0d3139ce8c35ac866ee77e116833e4a2b1eeea11) PR-URL: nodejs-private/node-private#932 CVE-ID: CVE-2026-58044
1 parent 440329f commit c8525ac

5 files changed

Lines changed: 133 additions & 63 deletions

src/node_http_parser.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ class Parser : public AsyncWrap, public StreamListener {
332332

333333
if (num_fields_ == num_values_) {
334334
// start of new field name
335+
rv = TrackHeaderPair();
336+
if (rv != 0) {
337+
return rv;
338+
}
339+
335340
num_fields_++;
336341
if (num_fields_ == kMaxHeaderFieldsCount) {
337342
// ran out of space - flush to javascript land
@@ -416,6 +421,7 @@ class Parser : public AsyncWrap, public StreamListener {
416421

417422
num_fields_ = 0;
418423
num_values_ = 0;
424+
header_pairs_ = 0;
419425

420426
// METHOD
421427
if (parser_.type == HTTP_REQUEST) {
@@ -509,6 +515,8 @@ class Parser : public AsyncWrap, public StreamListener {
509515
if (num_fields_)
510516
Flush(); // Flush trailing HTTP headers.
511517

518+
header_pairs_ = 0;
519+
512520
Local<Object> obj = object();
513521
Local<Value> cb = obj->Get(env()->context(),
514522
kOnMessageComplete).ToLocalChecked();
@@ -972,6 +980,7 @@ class Parser : public AsyncWrap, public StreamListener {
972980
got_exception_ = false;
973981
headers_completed_ = false;
974982
max_http_header_size_ = max_http_header_size;
983+
header_pairs_ = 0;
975984
}
976985

977986

@@ -984,6 +993,34 @@ class Parser : public AsyncWrap, public StreamListener {
984993
return 0;
985994
}
986995

996+
int TrackHeaderPair() {
997+
if (parser_.type != HTTP_REQUEST) {
998+
return 0;
999+
}
1000+
1001+
header_pairs_ += 2;
1002+
1003+
Local<Value> max_header_pairs_v;
1004+
if (!object()
1005+
->Get(env()->context(),
1006+
FIXED_ONE_BYTE_STRING(env()->isolate(), "maxHeaderPairs"))
1007+
.ToLocal(&max_header_pairs_v)) {
1008+
got_exception_ = true;
1009+
return -1;
1010+
}
1011+
1012+
if (!max_header_pairs_v->IsNumber()) {
1013+
return 0;
1014+
}
1015+
1016+
const double max_header_pairs = max_header_pairs_v.As<Number>()->Value();
1017+
if (max_header_pairs > 0 && header_pairs_ > max_header_pairs) {
1018+
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
1019+
return HPE_USER;
1020+
}
1021+
1022+
return 0;
1023+
}
9871024

9881025
int MaybePause() {
9891026
if (!pending_pause_) {
@@ -1016,6 +1053,7 @@ class Parser : public AsyncWrap, public StreamListener {
10161053
size_t current_buffer_len_;
10171054
const char* current_buffer_data_;
10181055
bool headers_completed_ = false;
1056+
size_t header_pairs_ = 0;
10191057
bool pending_pause_ = false;
10201058
uint64_t header_nread_ = 0;
10211059
uint64_t chunk_extensions_nread_ = 0;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
const net = require('net');
6+
7+
const server = http.createServer(common.mustNotCall());
8+
9+
server.maxHeadersCount = 2;
10+
11+
server.on('clientError', common.mustCall((err, socket) => {
12+
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
13+
socket.end('HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n');
14+
}));
15+
16+
server.listen(0, common.mustCall(() => {
17+
const port = server.address().port;
18+
const req = 'POST / HTTP/1.1\r\n' +
19+
'Host: localhost\r\n' +
20+
'X-A: b\r\n' +
21+
'Content-Length: 3\r\n' +
22+
'\r\nabc';
23+
24+
net.createConnection(port, 'localhost', common.mustCall(function() {
25+
let response = '';
26+
this.setEncoding('latin1');
27+
this.end(req);
28+
this.on('data', (chunk) => response += chunk);
29+
this.on('end', common.mustCall(() => {
30+
assert.match(response, /^HTTP\/1\.1 431 /);
31+
server.close();
32+
}));
33+
}));
34+
}));

test/parallel/test-http-max-headers-count.js

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
'use strict';
23-
require('../common');
23+
const common = require('../common');
2424
const assert = require('assert');
2525
const http = require('http');
2626

@@ -35,57 +35,69 @@ for (let i = 0; i < N; ++i) {
3535
headers[`key${i}`] = i;
3636
}
3737

38-
const maxAndExpected = [ // for server
39-
[50, 50],
40-
[1500, 102],
41-
[0, N + 2], // Host and Connection
38+
function createRequestHeaders(count) {
39+
const requestHeaders = {
40+
host: 'example.com',
41+
};
42+
for (let i = 0; i < count; ++i) {
43+
requestHeaders[`key${i}`] = i;
44+
}
45+
return requestHeaders;
46+
}
47+
48+
const serverMaxAndExpected = [ // for server
49+
[50, 50, 48],
50+
[1500, 102, N],
51+
[0, N + 2, N], // Host and Connection
4252
];
43-
let max = maxAndExpected[requests][0];
44-
let expected = maxAndExpected[requests][1];
53+
let max = serverMaxAndExpected[requests][0];
54+
let expected = serverMaxAndExpected[requests][1];
4555

46-
const server = http.createServer(function(req, res) {
56+
const server = http.createServer(common.mustCallAtLeast((req, res) => {
4757
assert.strictEqual(Object.keys(req.headers).length, expected);
48-
if (++requests < maxAndExpected.length) {
49-
max = maxAndExpected[requests][0];
50-
expected = maxAndExpected[requests][1];
58+
if (++requests < serverMaxAndExpected.length) {
59+
max = serverMaxAndExpected[requests][0];
60+
expected = serverMaxAndExpected[requests][1];
5161
server.maxHeadersCount = max;
5262
}
5363
res.writeHead(200, { ...headers, 'Connection': 'close' });
5464
res.end();
55-
});
65+
}));
5666
server.maxHeadersCount = max;
5767

58-
server.listen(0, function() {
59-
const maxAndExpected = [ // for client
68+
server.listen(0, common.mustCall(() => {
69+
const clientMaxAndExpected = [ // for client
6070
[20, 20],
6171
[1200, 104],
6272
[0, N + 4], // Host and Connection
6373
];
6474
doRequest();
6575

6676
function doRequest() {
67-
const max = maxAndExpected[responses][0];
68-
const expected = maxAndExpected[responses][1];
77+
const max = clientMaxAndExpected[responses][0];
78+
const expected = clientMaxAndExpected[responses][1];
79+
const requestHeaders =
80+
createRequestHeaders(serverMaxAndExpected[requests][2]);
6981
const req = http.request({
7082
port: server.address().port,
71-
headers: headers
72-
}, function(res) {
83+
headers: requestHeaders
84+
}, common.mustCall((res) => {
7385
assert.strictEqual(Object.keys(res.headers).length, expected);
7486
res.on('end', function() {
75-
if (++responses < maxAndExpected.length) {
87+
if (++responses < clientMaxAndExpected.length) {
7688
doRequest();
7789
} else {
7890
server.close();
7991
}
8092
});
8193
res.resume();
82-
});
94+
}));
8395
req.maxHeadersCount = max;
8496
req.end();
8597
}
86-
});
98+
}));
8799

88100
process.on('exit', function() {
89-
assert.strictEqual(requests, maxAndExpected.length);
90-
assert.strictEqual(responses, maxAndExpected.length);
101+
assert.strictEqual(requests, serverMaxAndExpected.length);
102+
assert.strictEqual(responses, serverMaxAndExpected.length);
91103
});

test/parallel/test-http-rawheaders-limit.js

Lines changed: 0 additions & 26 deletions
This file was deleted.

test/parallel/test-https-max-headers-count.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,29 @@ for (let i = 0; i < N; ++i) {
2424
headers[`key${i}`] = i;
2525
}
2626

27-
const maxAndExpected = [ // for server
28-
[50, 50],
29-
[1500, 102],
30-
[0, N + 2], // Host and Connection
27+
function createRequestHeaders(count) {
28+
const requestHeaders = {
29+
host: 'example.com',
30+
};
31+
for (let i = 0; i < count; ++i) {
32+
requestHeaders[`key${i}`] = i;
33+
}
34+
return requestHeaders;
35+
}
36+
37+
const serverMaxAndExpected = [ // for server
38+
[50, 50, 48],
39+
[1500, 102, N],
40+
[0, N + 2, N], // Host and Connection
3141
];
32-
let max = maxAndExpected[requests][0];
33-
let expected = maxAndExpected[requests][1];
42+
let max = serverMaxAndExpected[requests][0];
43+
let expected = serverMaxAndExpected[requests][1];
3444

3545
const server = https.createServer(serverOptions, common.mustCall((req, res) => {
3646
assert.strictEqual(Object.keys(req.headers).length, expected);
37-
if (++requests < maxAndExpected.length) {
38-
max = maxAndExpected[requests][0];
39-
expected = maxAndExpected[requests][1];
47+
if (++requests < serverMaxAndExpected.length) {
48+
max = serverMaxAndExpected[requests][0];
49+
expected = serverMaxAndExpected[requests][1];
4050
server.maxHeadersCount = max;
4151
}
4252
res.writeHead(200, { ...headers, 'Connection': 'close' });
@@ -45,22 +55,24 @@ const server = https.createServer(serverOptions, common.mustCall((req, res) => {
4555
server.maxHeadersCount = max;
4656

4757
server.listen(0, common.mustCall(() => {
48-
const maxAndExpected = [ // for client
58+
const clientMaxAndExpected = [ // for client
4959
[20, 20],
5060
[1200, 104],
5161
[0, N + 4], // Host and Connection
5262
];
5363
const doRequest = common.mustCall(() => {
54-
const max = maxAndExpected[responses][0];
55-
const expected = maxAndExpected[responses][1];
64+
const max = clientMaxAndExpected[responses][0];
65+
const expected = clientMaxAndExpected[responses][1];
66+
const requestHeaders =
67+
createRequestHeaders(serverMaxAndExpected[requests][2]);
5668
const req = https.request({
5769
port: server.address().port,
58-
headers: headers,
70+
headers: requestHeaders,
5971
rejectUnauthorized: false
6072
}, (res) => {
6173
assert.strictEqual(Object.keys(res.headers).length, expected);
6274
res.on('end', () => {
63-
if (++responses < maxAndExpected.length) {
75+
if (++responses < clientMaxAndExpected.length) {
6476
doRequest();
6577
} else {
6678
server.close();

0 commit comments

Comments
 (0)