Skip to content

Commit 8a008fb

Browse files
mcollinaRafaelGSS
authored andcommitted
http: reject requests exceeding max header count
PR-URL: nodejs-private/node-private#922 Refs: https://hackerone.com/reports/3564941 CVE-ID: CVE-2026-58044
1 parent ea26c12 commit 8a008fb

5 files changed

Lines changed: 126 additions & 56 deletions

src/node_http_parser.cc

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

374374
if (num_fields_ == num_values_) {
375375
// start of new field name
376+
rv = TrackHeaderPair();
377+
if (rv != 0) {
378+
return rv;
379+
}
380+
376381
num_fields_++;
377382
if (num_fields_ == kMaxHeaderFieldsCount) {
378383
// ran out of space - flush to javascript land
@@ -457,6 +462,7 @@ class Parser : public AsyncWrap, public StreamListener {
457462

458463
num_fields_ = 0;
459464
num_values_ = 0;
465+
header_pairs_ = 0;
460466

461467
// METHOD
462468
if (parser_.type == HTTP_REQUEST) {
@@ -550,6 +556,8 @@ class Parser : public AsyncWrap, public StreamListener {
550556
if (num_fields_)
551557
Flush(); // Flush trailing HTTP headers.
552558

559+
header_pairs_ = 0;
560+
553561
Local<Object> obj = object();
554562
Local<Value> cb = obj->Get(env()->context(),
555563
kOnMessageComplete).ToLocalChecked();
@@ -1017,6 +1025,7 @@ class Parser : public AsyncWrap, public StreamListener {
10171025
is_being_freed_ = false;
10181026
headers_completed_ = false;
10191027
max_http_header_size_ = max_http_header_size;
1028+
header_pairs_ = 0;
10201029
}
10211030

10221031

@@ -1029,6 +1038,34 @@ class Parser : public AsyncWrap, public StreamListener {
10291038
return 0;
10301039
}
10311040

1041+
int TrackHeaderPair() {
1042+
if (parser_.type != HTTP_REQUEST) {
1043+
return 0;
1044+
}
1045+
1046+
header_pairs_ += 2;
1047+
1048+
Local<Value> max_header_pairs_v;
1049+
if (!object()
1050+
->Get(env()->context(),
1051+
FIXED_ONE_BYTE_STRING(env()->isolate(), "maxHeaderPairs"))
1052+
.ToLocal(&max_header_pairs_v)) {
1053+
got_exception_ = true;
1054+
return -1;
1055+
}
1056+
1057+
if (!max_header_pairs_v->IsNumber()) {
1058+
return 0;
1059+
}
1060+
1061+
const double max_header_pairs = max_header_pairs_v.As<Number>()->Value();
1062+
if (max_header_pairs > 0 && header_pairs_ > max_header_pairs) {
1063+
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
1064+
return HPE_USER;
1065+
}
1066+
1067+
return 0;
1068+
}
10321069

10331070
int MaybePause() {
10341071
if (!pending_pause_) {
@@ -1063,6 +1100,7 @@ class Parser : public AsyncWrap, public StreamListener {
10631100
size_t current_buffer_len_;
10641101
const char* current_buffer_data_;
10651102
bool headers_completed_ = false;
1103+
size_t header_pairs_ = 0;
10661104
bool pending_pause_ = false;
10671105
uint64_t header_nread_ = 0;
10681106
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: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,29 @@ 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

4656
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' });
@@ -56,23 +66,25 @@ const server = http.createServer(common.mustCallAtLeast((req, res) => {
5666
server.maxHeadersCount = max;
5767

5868
server.listen(0, common.mustCall(() => {
59-
const maxAndExpected = [ // for client
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
83+
headers: requestHeaders
7284
}, 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();
@@ -86,6 +98,6 @@ server.listen(0, common.mustCall(() => {
8698
}));
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
}, common.mustCall((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)