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
return0;
10301039
}
10311040

1041+
intTrackHeaderPair() {
1042+
if (parser_.type != HTTP_REQUEST) {
1043+
return0;
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+
return0;
1059+
}
1060+
1061+
constdouble 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+
returnHPE_USER;
1065+
}
1066+
1067+
return0;
1068+
}
10321069

10331070
intMaybePause() {
10341071
if (!pending_pause_) {
@@ -1063,6 +1100,7 @@ class Parser : public AsyncWrap, public StreamListener {
10631100
size_t current_buffer_len_;
10641101
constchar* 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+
constcommon=require('../common');
3+
constassert=require('assert');
4+
consthttp=require('http');
5+
constnet=require('net');
6+
7+
constserver=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+
constport=server.address().port;
18+
constreq='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+
letresponse='';
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\.1431/);
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-
constmaxAndExpected=[// for server
39-
[50,50],
40-
[1500,102],
41-
[0,N+2],// Host and Connection
38+
functioncreateRequestHeaders(count){
39+
constrequestHeaders={
40+
host: 'example.com',
41+
};
42+
for(leti=0;i<count;++i){
43+
requestHeaders[`key${i}`]=i;
44+
}
45+
returnrequestHeaders;
46+
}
47+
48+
constserverMaxAndExpected=[// for server
49+
[50,50,48],
50+
[1500,102,N],
51+
[0,N+2,N],// Host and Connection
4252
];
43-
letmax=maxAndExpected[requests][0];
44-
letexpected=maxAndExpected[requests][1];
53+
letmax=serverMaxAndExpected[requests][0];
54+
letexpected=serverMaxAndExpected[requests][1];
4555

4656
constserver=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-
constmaxAndExpected=[// for client
69+
constclientMaxAndExpected=[// for client
6070
[20,20],
6171
[1200,104],
6272
[0,N+4],// Host and Connection
6373
];
6474
doRequest();
6575

6676
functiondoRequest(){
67-
constmax=maxAndExpected[responses][0];
68-
constexpected=maxAndExpected[responses][1];
77+
constmax=clientMaxAndExpected[responses][0];
78+
constexpected=clientMaxAndExpected[responses][1];
79+
constrequestHeaders=
80+
createRequestHeaders(serverMaxAndExpected[requests][2]);
6981
constreq=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-
constmaxAndExpected=[// for server
28-
[50,50],
29-
[1500,102],
30-
[0,N+2],// Host and Connection
27+
functioncreateRequestHeaders(count){
28+
constrequestHeaders={
29+
host: 'example.com',
30+
};
31+
for(leti=0;i<count;++i){
32+
requestHeaders[`key${i}`]=i;
33+
}
34+
returnrequestHeaders;
35+
}
36+
37+
constserverMaxAndExpected=[// for server
38+
[50,50,48],
39+
[1500,102,N],
40+
[0,N+2,N],// Host and Connection
3141
];
32-
letmax=maxAndExpected[requests][0];
33-
letexpected=maxAndExpected[requests][1];
42+
letmax=serverMaxAndExpected[requests][0];
43+
letexpected=serverMaxAndExpected[requests][1];
3444

3545
constserver=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-
constmaxAndExpected=[// for client
58+
constclientMaxAndExpected=[// for client
4959
[20,20],
5060
[1200,104],
5161
[0,N+4],// Host and Connection
5262
];
5363
constdoRequest=common.mustCall(()=>{
54-
constmax=maxAndExpected[responses][0];
55-
constexpected=maxAndExpected[responses][1];
64+
constmax=clientMaxAndExpected[responses][0];
65+
constexpected=clientMaxAndExpected[responses][1];
66+
constrequestHeaders=
67+
createRequestHeaders(serverMaxAndExpected[requests][2]);
5668
constreq=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)