Skip to content

Commit 911cb33

Browse files
ShogunPandamarco-ippolito
authored andcommittedFeb 12, 2024
http: add maximum chunk extension size
PR-URL: nodejs-private/node-private#520 Refs: nodejs-private/node-private#518 CVE-ID: CVE-2024-22019
1 parent e6b4c10 commit 911cb33

File tree

10 files changed

+293
-19
lines changed

10 files changed

+293
-19
lines changed
 

‎deps/llhttp/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
libllhttp.pc

‎deps/llhttp/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.5.1)
22
cmake_policy(SET CMP0069 NEW)
33

4-
project(llhttp VERSION 6.0.11)
4+
project(llhttp VERSION 6.1.0)
55
include(GNUInstallDirs)
66

77
set(CMAKE_C_STANDARD 99)

‎deps/llhttp/include/llhttp.h

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#define INCLUDE_LLHTTP_H_
33

44
#define LLHTTP_VERSION_MAJOR 6
5-
#define LLHTTP_VERSION_MINOR 0
6-
#define LLHTTP_VERSION_PATCH 11
5+
#define LLHTTP_VERSION_MINOR 1
6+
#define LLHTTP_VERSION_PATCH 0
77

88
#ifndef LLHTTP_STRICT_MODE
99
# define LLHTTP_STRICT_MODE 0
@@ -348,6 +348,9 @@ struct llhttp_settings_s {
348348
*/
349349
llhttp_cb on_headers_complete;
350350

351+
/* Possible return values 0, -1, HPE_USER */
352+
llhttp_data_cb on_chunk_parameters;
353+
351354
/* Possible return values 0, -1, HPE_USER */
352355
llhttp_data_cb on_body;
353356

‎deps/llhttp/src/api.c

+7
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,13 @@ int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
355355
}
356356

357357

358+
int llhttp__on_chunk_parameters(llhttp_t* s, const char* p, const char* endp) {
359+
int err;
360+
SPAN_CALLBACK_MAYBE(s, on_chunk_parameters, p, endp - p);
361+
return err;
362+
}
363+
364+
358365
int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
359366
int err;
360367
CALLBACK_MAYBE(s, on_chunk_complete);

‎deps/llhttp/src/llhttp.c

+108-14
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ enum llparse_state_e {
340340
s_n_llhttp__internal__n_invoke_is_equal_content_length,
341341
s_n_llhttp__internal__n_chunk_size_almost_done,
342342
s_n_llhttp__internal__n_chunk_parameters,
343+
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters,
344+
s_n_llhttp__internal__n_chunk_parameters_ows,
343345
s_n_llhttp__internal__n_chunk_size_otherwise,
344346
s_n_llhttp__internal__n_chunk_size,
345347
s_n_llhttp__internal__n_chunk_size_digit,
@@ -539,6 +541,10 @@ int llhttp__on_body(
539541
llhttp__internal_t* s, const unsigned char* p,
540542
const unsigned char* endp);
541543

544+
int llhttp__on_chunk_parameters(
545+
llhttp__internal_t* s, const unsigned char* p,
546+
const unsigned char* endp);
547+
542548
int llhttp__on_status(
543549
llhttp__internal_t* s, const unsigned char* p,
544550
const unsigned char* endp);
@@ -1226,8 +1232,7 @@ static llparse_state_t llhttp__internal__run(
12261232
goto s_n_llhttp__internal__n_chunk_parameters;
12271233
}
12281234
case 2: {
1229-
p++;
1230-
goto s_n_llhttp__internal__n_chunk_size_almost_done;
1235+
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters;
12311236
}
12321237
default: {
12331238
goto s_n_llhttp__internal__n_error_10;
@@ -1236,6 +1241,34 @@ static llparse_state_t llhttp__internal__run(
12361241
/* UNREACHABLE */;
12371242
abort();
12381243
}
1244+
case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters:
1245+
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters: {
1246+
if (p == endp) {
1247+
return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
1248+
}
1249+
state->_span_pos0 = (void*) p;
1250+
state->_span_cb0 = llhttp__on_chunk_parameters;
1251+
goto s_n_llhttp__internal__n_chunk_parameters;
1252+
/* UNREACHABLE */;
1253+
abort();
1254+
}
1255+
case s_n_llhttp__internal__n_chunk_parameters_ows:
1256+
s_n_llhttp__internal__n_chunk_parameters_ows: {
1257+
if (p == endp) {
1258+
return s_n_llhttp__internal__n_chunk_parameters_ows;
1259+
}
1260+
switch (*p) {
1261+
case ' ': {
1262+
p++;
1263+
goto s_n_llhttp__internal__n_chunk_parameters_ows;
1264+
}
1265+
default: {
1266+
goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
1267+
}
1268+
}
1269+
/* UNREACHABLE */;
1270+
abort();
1271+
}
12391272
case s_n_llhttp__internal__n_chunk_size_otherwise:
12401273
s_n_llhttp__internal__n_chunk_size_otherwise: {
12411274
if (p == endp) {
@@ -1246,13 +1279,9 @@ static llparse_state_t llhttp__internal__run(
12461279
p++;
12471280
goto s_n_llhttp__internal__n_chunk_size_almost_done;
12481281
}
1249-
case ' ': {
1250-
p++;
1251-
goto s_n_llhttp__internal__n_chunk_parameters;
1252-
}
12531282
case ';': {
12541283
p++;
1255-
goto s_n_llhttp__internal__n_chunk_parameters;
1284+
goto s_n_llhttp__internal__n_chunk_parameters_ows;
12561285
}
12571286
default: {
12581287
goto s_n_llhttp__internal__n_error_11;
@@ -6074,6 +6103,24 @@ static llparse_state_t llhttp__internal__run(
60746103
/* UNREACHABLE */;
60756104
abort();
60766105
}
6106+
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters: {
6107+
const unsigned char* start;
6108+
int err;
6109+
6110+
start = state->_span_pos0;
6111+
state->_span_pos0 = NULL;
6112+
err = llhttp__on_chunk_parameters(state, start, p);
6113+
if (err != 0) {
6114+
state->error = err;
6115+
state->error_pos = (const char*) (p + 1);
6116+
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
6117+
return s_error;
6118+
}
6119+
p++;
6120+
goto s_n_llhttp__internal__n_chunk_size_almost_done;
6121+
/* UNREACHABLE */;
6122+
abort();
6123+
}
60776124
s_n_llhttp__internal__n_error_10: {
60786125
state->error = 0x2;
60796126
state->reason = "Invalid character in chunk parameters";
@@ -8441,6 +8488,8 @@ enum llparse_state_e {
84418488
s_n_llhttp__internal__n_invoke_is_equal_content_length,
84428489
s_n_llhttp__internal__n_chunk_size_almost_done,
84438490
s_n_llhttp__internal__n_chunk_parameters,
8491+
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters,
8492+
s_n_llhttp__internal__n_chunk_parameters_ows,
84448493
s_n_llhttp__internal__n_chunk_size_otherwise,
84458494
s_n_llhttp__internal__n_chunk_size,
84468495
s_n_llhttp__internal__n_chunk_size_digit,
@@ -8635,6 +8684,10 @@ int llhttp__on_body(
86358684
llhttp__internal_t* s, const unsigned char* p,
86368685
const unsigned char* endp);
86378686

8687+
int llhttp__on_chunk_parameters(
8688+
llhttp__internal_t* s, const unsigned char* p,
8689+
const unsigned char* endp);
8690+
86388691
int llhttp__on_status(
86398692
llhttp__internal_t* s, const unsigned char* p,
86408693
const unsigned char* endp);
@@ -9299,8 +9352,7 @@ static llparse_state_t llhttp__internal__run(
92999352
goto s_n_llhttp__internal__n_chunk_parameters;
93009353
}
93019354
case 2: {
9302-
p++;
9303-
goto s_n_llhttp__internal__n_chunk_size_almost_done;
9355+
goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters;
93049356
}
93059357
default: {
93069358
goto s_n_llhttp__internal__n_error_6;
@@ -9309,6 +9361,34 @@ static llparse_state_t llhttp__internal__run(
93099361
/* UNREACHABLE */;
93109362
abort();
93119363
}
9364+
case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters:
9365+
s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters: {
9366+
if (p == endp) {
9367+
return s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
9368+
}
9369+
state->_span_pos0 = (void*) p;
9370+
state->_span_cb0 = llhttp__on_chunk_parameters;
9371+
goto s_n_llhttp__internal__n_chunk_parameters;
9372+
/* UNREACHABLE */;
9373+
abort();
9374+
}
9375+
case s_n_llhttp__internal__n_chunk_parameters_ows:
9376+
s_n_llhttp__internal__n_chunk_parameters_ows: {
9377+
if (p == endp) {
9378+
return s_n_llhttp__internal__n_chunk_parameters_ows;
9379+
}
9380+
switch (*p) {
9381+
case ' ': {
9382+
p++;
9383+
goto s_n_llhttp__internal__n_chunk_parameters_ows;
9384+
}
9385+
default: {
9386+
goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_parameters;
9387+
}
9388+
}
9389+
/* UNREACHABLE */;
9390+
abort();
9391+
}
93129392
case s_n_llhttp__internal__n_chunk_size_otherwise:
93139393
s_n_llhttp__internal__n_chunk_size_otherwise: {
93149394
if (p == endp) {
@@ -9319,13 +9399,9 @@ static llparse_state_t llhttp__internal__run(
93199399
p++;
93209400
goto s_n_llhttp__internal__n_chunk_size_almost_done;
93219401
}
9322-
case ' ': {
9323-
p++;
9324-
goto s_n_llhttp__internal__n_chunk_parameters;
9325-
}
93269402
case ';': {
93279403
p++;
9328-
goto s_n_llhttp__internal__n_chunk_parameters;
9404+
goto s_n_llhttp__internal__n_chunk_parameters_ows;
93299405
}
93309406
default: {
93319407
goto s_n_llhttp__internal__n_error_7;
@@ -13951,6 +14027,24 @@ static llparse_state_t llhttp__internal__run(
1395114027
/* UNREACHABLE */;
1395214028
abort();
1395314029
}
14030+
s_n_llhttp__internal__n_span_end_llhttp__on_chunk_parameters: {
14031+
const unsigned char* start;
14032+
int err;
14033+
14034+
start = state->_span_pos0;
14035+
state->_span_pos0 = NULL;
14036+
err = llhttp__on_chunk_parameters(state, start, p);
14037+
if (err != 0) {
14038+
state->error = err;
14039+
state->error_pos = (const char*) (p + 1);
14040+
state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done;
14041+
return s_error;
14042+
}
14043+
p++;
14044+
goto s_n_llhttp__internal__n_chunk_size_almost_done;
14045+
/* UNREACHABLE */;
14046+
abort();
14047+
}
1395414048
s_n_llhttp__internal__n_error_6: {
1395514049
state->error = 0x2;
1395614050
state->reason = "Invalid character in chunk parameters";

‎doc/api/errors.md

+12
Original file line numberDiff line numberDiff line change
@@ -3132,6 +3132,18 @@ malconfigured clients, if more than 8 KiB of HTTP header data is received then
31323132
HTTP parsing will abort without a request or response object being created, and
31333133
an `Error` with this code will be emitted.
31343134

3135+
<a id="HPE_CHUNK_EXTENSIONS_OVERFLOW"></a>
3136+
3137+
### `HPE_CHUNK_EXTENSIONS_OVERFLOW`
3138+
3139+
<!-- YAML
3140+
added: REPLACEME
3141+
-->
3142+
3143+
Too much data was received for a chunk extensions. In order to protect against
3144+
malicious or malconfigured clients, if more than 16 KiB of data is received
3145+
then an `Error` with this code will be emitted.
3146+
31353147
<a id="HPE_UNEXPECTED_CONTENT_LENGTH"></a>
31363148

31373149
### `HPE_UNEXPECTED_CONTENT_LENGTH`

‎lib/_http_server.js

+8
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,11 @@ const requestHeaderFieldsTooLargeResponse = Buffer.from(
846846
'Connection: close\r\n\r\n', 'ascii',
847847
);
848848

849+
const requestChunkExtensionsTooLargeResponse = Buffer.from(
850+
`HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` +
851+
'Connection: close\r\n\r\n', 'ascii',
852+
);
853+
849854
function warnUnclosedSocket() {
850855
if (warnUnclosedSocket.emitted) {
851856
return;
@@ -881,6 +886,9 @@ function socketOnError(e) {
881886
case 'HPE_HEADER_OVERFLOW':
882887
response = requestHeaderFieldsTooLargeResponse;
883888
break;
889+
case 'HPE_CHUNK_EXTENSIONS_OVERFLOW':
890+
response = requestChunkExtensionsTooLargeResponse;
891+
break;
884892
case 'ERR_HTTP_REQUEST_TIMEOUT':
885893
response = requestTimeoutResponse;
886894
break;

‎src/node_http_parser.cc

+19-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ const uint32_t kOnExecute = 5;
7979
const uint32_t kOnTimeout = 6;
8080
// Any more fields than this will be flushed into JS
8181
const size_t kMaxHeaderFieldsCount = 32;
82+
// Maximum size of chunk extensions
83+
const size_t kMaxChunkExtensionsSize = 16384;
8284

8385
const uint32_t kLenientNone = 0;
8486
const uint32_t kLenientHeaders = 1 << 0;
@@ -261,6 +263,7 @@ class Parser : public AsyncWrap, public StreamListener {
261263

262264
num_fields_ = num_values_ = 0;
263265
headers_completed_ = false;
266+
chunk_extensions_nread_ = 0;
264267
last_message_start_ = uv_hrtime();
265268
url_.Reset();
266269
status_message_.Reset();
@@ -516,9 +519,22 @@ class Parser : public AsyncWrap, public StreamListener {
516519
return 0;
517520
}
518521

519-
// Reset nread for the next chunk
522+
int on_chunk_extension(const char* at, size_t length) {
523+
chunk_extensions_nread_ += length;
524+
525+
if (chunk_extensions_nread_ > kMaxChunkExtensionsSize) {
526+
llhttp_set_error_reason(&parser_,
527+
"HPE_CHUNK_EXTENSIONS_OVERFLOW:Chunk extensions overflow");
528+
return HPE_USER;
529+
}
530+
531+
return 0;
532+
}
533+
534+
// Reset nread for the next chunk and also reset the extensions counter
520535
int on_chunk_header() {
521536
header_nread_ = 0;
537+
chunk_extensions_nread_ = 0;
522538
return 0;
523539
}
524540

@@ -986,6 +1002,7 @@ class Parser : public AsyncWrap, public StreamListener {
9861002
bool headers_completed_ = false;
9871003
bool pending_pause_ = false;
9881004
uint64_t header_nread_ = 0;
1005+
uint64_t chunk_extensions_nread_ = 0;
9891006
uint64_t max_http_header_size_;
9901007
uint64_t last_message_start_;
9911008
ConnectionsList* connectionsList_;
@@ -1157,6 +1174,7 @@ const llhttp_settings_t Parser::settings = {
11571174
Proxy<DataCall, &Parser::on_header_field>::Raw,
11581175
Proxy<DataCall, &Parser::on_header_value>::Raw,
11591176
Proxy<Call, &Parser::on_headers_complete>::Raw,
1177+
Proxy<DataCall, &Parser::on_chunk_extension>::Raw,
11601178
Proxy<DataCall, &Parser::on_body>::Raw,
11611179
Proxy<Call, &Parser::on_message_complete>::Raw,
11621180
Proxy<Call, &Parser::on_chunk_header>::Raw,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const http = require('http');
5+
const net = require('net');
6+
const assert = require('assert');
7+
8+
// Verify that chunk extensions are limited in size when sent all together.
9+
{
10+
const server = http.createServer((req, res) => {
11+
req.on('end', () => {
12+
res.writeHead(200, { 'Content-Type': 'text/plain' });
13+
res.end('bye');
14+
});
15+
16+
req.resume();
17+
});
18+
19+
server.listen(0, () => {
20+
const sock = net.connect(server.address().port);
21+
let data = '';
22+
23+
sock.on('data', (chunk) => data += chunk.toString('utf-8'));
24+
25+
sock.on('end', common.mustCall(function() {
26+
assert.strictEqual(data, 'HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n');
27+
server.close();
28+
}));
29+
30+
sock.end('' +
31+
'GET / HTTP/1.1\r\n' +
32+
'Host: localhost:8080\r\n' +
33+
'Transfer-Encoding: chunked\r\n\r\n' +
34+
'2;' + 'A'.repeat(20000) + '=bar\r\nAA\r\n' +
35+
'0\r\n\r\n'
36+
);
37+
});
38+
}
39+
40+
// Verify that chunk extensions are limited in size when sent in intervals.
41+
{
42+
const server = http.createServer((req, res) => {
43+
req.on('end', () => {
44+
res.writeHead(200, { 'Content-Type': 'text/plain' });
45+
res.end('bye');
46+
});
47+
48+
req.resume();
49+
});
50+
51+
server.listen(0, () => {
52+
const sock = net.connect(server.address().port);
53+
let remaining = 20000;
54+
let data = '';
55+
56+
const interval = setInterval(
57+
() => {
58+
if (remaining > 0) {
59+
sock.write('A'.repeat(1000));
60+
} else {
61+
sock.write('=bar\r\nAA\r\n0\r\n\r\n');
62+
clearInterval(interval);
63+
}
64+
65+
remaining -= 1000;
66+
},
67+
common.platformTimeout(20),
68+
).unref();
69+
70+
sock.on('data', (chunk) => data += chunk.toString('utf-8'));
71+
72+
sock.on('end', common.mustCall(function() {
73+
assert.strictEqual(data, 'HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n');
74+
server.close();
75+
}));
76+
77+
sock.write('' +
78+
'GET / HTTP/1.1\r\n' +
79+
'Host: localhost:8080\r\n' +
80+
'Transfer-Encoding: chunked\r\n\r\n' +
81+
'2;'
82+
);
83+
});
84+
}
85+
86+
// Verify the chunk extensions is correctly reset after a chunk
87+
{
88+
const server = http.createServer((req, res) => {
89+
req.on('end', () => {
90+
res.writeHead(200, { 'content-type': 'text/plain', 'connection': 'close', 'date': 'now' });
91+
res.end('bye');
92+
});
93+
94+
req.resume();
95+
});
96+
97+
server.listen(0, () => {
98+
const sock = net.connect(server.address().port);
99+
let data = '';
100+
101+
sock.on('data', (chunk) => data += chunk.toString('utf-8'));
102+
103+
sock.on('end', common.mustCall(function() {
104+
assert.strictEqual(
105+
data,
106+
'HTTP/1.1 200 OK\r\n' +
107+
'content-type: text/plain\r\n' +
108+
'connection: close\r\n' +
109+
'date: now\r\n' +
110+
'Transfer-Encoding: chunked\r\n' +
111+
'\r\n' +
112+
'3\r\n' +
113+
'bye\r\n' +
114+
'0\r\n' +
115+
'\r\n',
116+
);
117+
118+
server.close();
119+
}));
120+
121+
sock.end('' +
122+
'GET / HTTP/1.1\r\n' +
123+
'Host: localhost:8080\r\n' +
124+
'Transfer-Encoding: chunked\r\n\r\n' +
125+
'2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
126+
'2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
127+
'2;' + 'A'.repeat(10000) + '=bar\r\nAA\r\n' +
128+
'0\r\n\r\n'
129+
);
130+
});
131+
}

‎tools/update-llhttp.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,5 @@ echo ""
5959
echo "Please git add llhttp, commit the new version:"
6060
echo ""
6161
echo "$ git add -A deps/llhttp"
62-
echo "$ git commit -m \"deps: update nghttp2 to $LLHTTP_VERSION\""
62+
echo "$ git commit -m \"deps: update llhttp to $LLHTTP_VERSION\""
6363
echo ""

0 commit comments

Comments
 (0)
Please sign in to comment.