Skip to content

Commit 6922f5b

Browse files
authored
New lenient flag to have spaces after chunk header. (#245)
* fix: Do not allow body for HTTP 304. * feat: Added new lenient flags to allow spaces after chunk header.
1 parent 50524c0 commit 6922f5b

File tree

10 files changed

+214
-20
lines changed

10 files changed

+214
-20
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,15 @@ With this flag the new chunk can start immediately after the previous one.
381381
382382
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
383383
384+
### `void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)`
385+
386+
Enables/disables lenient handling of spaces after chunk size.
387+
388+
Normally `llhttp` would error when after a chunk size is followed by one or more spaces are present instead of a CRLF or `;`.
389+
With this flag this check is disabled.
390+
391+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
392+
384393
## Build Instructions
385394
386395
Make sure you have [Node.js](https://nodejs.org/), npm and npx installed. Then under project directory run:

src/llhttp/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export enum LENIENT_FLAGS {
7474
OPTIONAL_LF_AFTER_CR = 1 << 6,
7575
OPTIONAL_CRLF_AFTER_CHUNK = 1 << 7,
7676
OPTIONAL_CR_BEFORE_LF = 1 << 8,
77+
SPACES_AFTER_CHUNK_SIZE = 1 << 9,
7778
}
7879

7980
export enum METHODS {

src/llhttp/http.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,16 @@ export class HTTP {
924924
.otherwise(n('chunk_size_otherwise'));
925925

926926
n('chunk_size_otherwise')
927+
.match(
928+
[ ' ', '\t' ],
929+
this.testLenientFlags(
930+
LENIENT_FLAGS.SPACES_AFTER_CHUNK_SIZE,
931+
{
932+
1: n('chunk_size_otherwise'),
933+
},
934+
p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size'),
935+
),
936+
)
927937
.match('\r', n('chunk_size_almost_done'))
928938
.match(
929939
'\n',

src/native/api.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,14 @@ void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) {
323323
}
324324
}
325325

326+
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) {
327+
if (enabled) {
328+
parser->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
329+
} else {
330+
parser->lenient_flags &= ~LENIENT_SPACES_AFTER_CHUNK_SIZE;
331+
}
332+
}
333+
326334
/* Callbacks */
327335

328336

src/native/http.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,26 @@ int llhttp__after_headers_complete(llhttp_t* parser, const char* p,
3939
int hasBody;
4040

4141
hasBody = parser->flags & F_CHUNKED || parser->content_length > 0;
42-
if (parser->upgrade && (parser->method == HTTP_CONNECT ||
43-
(parser->flags & F_SKIPBODY) || !hasBody)) {
42+
if (
43+
(parser->upgrade && (parser->method == HTTP_CONNECT ||
44+
(parser->flags & F_SKIPBODY) || !hasBody)) ||
45+
/* See RFC 2616 section 4.4 - 1xx e.g. Continue */
46+
(parser->type == HTTP_RESPONSE && parser->status_code / 100 == 1)
47+
) {
4448
/* Exit, the rest of the message is in a different protocol. */
4549
return 1;
4650
}
4751

48-
if (parser->flags & F_SKIPBODY) {
52+
/* See RFC 2616 section 4.4 */
53+
if (
54+
parser->flags & F_SKIPBODY || /* response to a HEAD request */
55+
(
56+
parser->type == HTTP_RESPONSE && (
57+
parser->status_code == 204 || /* No Content */
58+
parser->status_code == 304 /* Not Modified */
59+
)
60+
)
61+
) {
4962
return 0;
5063
} else if (parser->flags & F_CHUNKED) {
5164
/* chunked encoding - ignore Content-Length header, prepare for a chunk */

test/fixtures/extra.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ void llhttp__test_init_response_lenient_optional_crlf_after_chunk(llparse_t* s)
181181
s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
182182
}
183183

184+
void llhttp__test_init_request_lenient_spaces_after_chunk_size(llparse_t* s) {
185+
llhttp__test_init_request(s);
186+
s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
187+
}
188+
189+
void llhttp__test_init_response_lenient_spaces_after_chunk_size(llparse_t* s) {
190+
llhttp__test_init_response(s);
191+
s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
192+
}
193+
184194

185195
void llhttp__test_finish(llparse_t* s) {
186196
llparse__print(NULL, NULL, "finish=%d", s->finish);

test/fixtures/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type TestType = 'request' | 'response' | 'request-finish' | 'response-fin
2020
'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' |
2121
'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' |
2222
'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' |
23+
'request-lenient-spaces-after-chunk-size' | 'response-lenient-spaces-after-chunk-size' |
2324
'none' | 'url';
2425

2526
export const allowedTypes: TestType[] = [
@@ -45,6 +46,8 @@ export const allowedTypes: TestType[] = [
4546
'response-lenient-optional-cr-before-lf',
4647
'request-lenient-optional-crlf-after-chunk',
4748
'response-lenient-optional-crlf-after-chunk',
49+
'request-lenient-spaces-after-chunk-size',
50+
'response-lenient-spaces-after-chunk-size',
4851
];
4952

5053
const BUILD_DIR = path.join(__dirname, '..', 'tmp');

test/request/transfer-encoding.md

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ off=83 header_field complete
302302
off=84 len=7 span[header_value]="chunked"
303303
off=93 header_value complete
304304
off=95 headers complete method=3 v=1/1 flags=208 content_length=0
305-
off=96 error code=12 reason="Invalid character in chunk size"
305+
off=97 error code=12 reason="Invalid character in chunk size"
306306
```
307307

308308
### No extension after semicolon
@@ -884,7 +884,7 @@ off=37 header_field complete
884884
off=38 len=7 span[header_value]="chunked"
885885
off=47 header_value complete
886886
off=49 headers complete method=4 v=1/1 flags=208 content_length=0
887-
off=50 error code=12 reason="Invalid character in chunk size"
887+
off=51 error code=12 reason="Invalid character in chunk size"
888888
```
889889

890890
## Invalid OBS fold after chunked value
@@ -1117,4 +1117,67 @@ off=79 chunk header len=5
11171117
off=79 len=5 span[body]="ABCDE"
11181118
off=84 chunk complete
11191119
off=87 chunk header len=0
1120+
```
1121+
1122+
## Space after chunk header
1123+
1124+
<!-- meta={"type": "request"} -->
1125+
```http
1126+
PUT /url HTTP/1.1
1127+
Transfer-Encoding: chunked
1128+
1129+
a \r\n0123456789
1130+
0
1131+
1132+
1133+
```
1134+
1135+
```log
1136+
off=0 message begin
1137+
off=0 len=3 span[method]="PUT"
1138+
off=3 method complete
1139+
off=4 len=4 span[url]="/url"
1140+
off=9 url complete
1141+
off=14 len=3 span[version]="1.1"
1142+
off=17 version complete
1143+
off=19 len=17 span[header_field]="Transfer-Encoding"
1144+
off=37 header_field complete
1145+
off=38 len=7 span[header_value]="chunked"
1146+
off=47 header_value complete
1147+
off=49 headers complete method=4 v=1/1 flags=208 content_length=0
1148+
off=51 error code=12 reason="Invalid character in chunk size"
1149+
```
1150+
1151+
## Space after chunk header (lenient)
1152+
1153+
<!-- meta={"type": "request-lenient-spaces-after-chunk-size"} -->
1154+
```http
1155+
PUT /url HTTP/1.1
1156+
Transfer-Encoding: chunked
1157+
1158+
a \r\n0123456789
1159+
0
1160+
1161+
1162+
```
1163+
1164+
```log
1165+
off=0 message begin
1166+
off=0 len=3 span[method]="PUT"
1167+
off=3 method complete
1168+
off=4 len=4 span[url]="/url"
1169+
off=9 url complete
1170+
off=14 len=3 span[version]="1.1"
1171+
off=17 version complete
1172+
off=19 len=17 span[header_field]="Transfer-Encoding"
1173+
off=37 header_field complete
1174+
off=38 len=7 span[header_value]="chunked"
1175+
off=47 header_value complete
1176+
off=49 headers complete method=4 v=1/1 flags=208 content_length=0
1177+
off=53 chunk header len=10
1178+
off=53 len=10 span[body]="0123456789"
1179+
off=65 chunk complete
1180+
off=68 chunk header len=0
1181+
off=70 chunk complete
1182+
off=70 message complete
11201183
```

test/response/connection.md

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,8 @@ off=84 header_field complete
298298
off=85 len=1 span[header_value]="4"
299299
off=88 header_value complete
300300
off=90 headers complete status=101 v=1/1 flags=34 content_length=4
301-
off=90 len=4 span[body]="body"
302-
off=94 message complete
303-
off=94 error code=22 reason="Pause on CONNECT/Upgrade"
301+
off=90 message complete
302+
off=90 error code=22 reason="Pause on CONNECT/Upgrade"
304303
```
305304

306305
## HTTP 101 response with Upgrade and Transfer-Encoding header
@@ -340,16 +339,8 @@ off=87 header_field complete
340339
off=88 len=7 span[header_value]="chunked"
341340
off=97 header_value complete
342341
off=99 headers complete status=101 v=1/1 flags=21c content_length=0
343-
off=102 chunk header len=2
344-
off=102 len=2 span[body]="bo"
345-
off=106 chunk complete
346-
off=109 chunk header len=2
347-
off=109 len=2 span[body]="dy"
348-
off=113 chunk complete
349-
off=116 chunk header len=0
350-
off=118 chunk complete
351-
off=118 message complete
352-
off=118 error code=22 reason="Pause on CONNECT/Upgrade"
342+
off=99 message complete
343+
off=99 error code=22 reason="Pause on CONNECT/Upgrade"
353344
```
354345

355346
## HTTP 200 response with Upgrade header
@@ -463,3 +454,89 @@ off=99 chunk header len=0
463454
off=101 chunk complete
464455
off=101 message complete
465456
```
457+
458+
## HTTP 304 with Content-Length
459+
460+
<!-- meta={"type": "response"} -->
461+
```http
462+
HTTP/1.1 304 Not Modified
463+
Content-Length: 10
464+
465+
466+
HTTP/1.1 200 OK
467+
Content-Length: 5
468+
469+
hello
470+
```
471+
472+
```log
473+
off=0 message begin
474+
off=5 len=3 span[version]="1.1"
475+
off=8 version complete
476+
off=13 len=12 span[status]="Not Modified"
477+
off=27 status complete
478+
off=27 len=14 span[header_field]="Content-Length"
479+
off=42 header_field complete
480+
off=43 len=2 span[header_value]="10"
481+
off=47 header_value complete
482+
off=49 headers complete status=304 v=1/1 flags=20 content_length=10
483+
off=49 message complete
484+
off=51 reset
485+
off=51 message begin
486+
off=56 len=3 span[version]="1.1"
487+
off=59 version complete
488+
off=64 len=2 span[status]="OK"
489+
off=68 status complete
490+
off=68 len=14 span[header_field]="Content-Length"
491+
off=83 header_field complete
492+
off=84 len=1 span[header_value]="5"
493+
off=87 header_value complete
494+
off=89 headers complete status=200 v=1/1 flags=20 content_length=5
495+
off=89 len=5 span[body]="hello"
496+
off=94 message complete
497+
```
498+
499+
## HTTP 304 with Transfer-Encoding
500+
501+
<!-- meta={"type": "response"} -->
502+
```http
503+
HTTP/1.1 304 Not Modified
504+
Transfer-Encoding: chunked
505+
506+
HTTP/1.1 200 OK
507+
Transfer-Encoding: chunked
508+
509+
5
510+
hello
511+
0
512+
513+
```
514+
515+
```log
516+
off=0 message begin
517+
off=5 len=3 span[version]="1.1"
518+
off=8 version complete
519+
off=13 len=12 span[status]="Not Modified"
520+
off=27 status complete
521+
off=27 len=17 span[header_field]="Transfer-Encoding"
522+
off=45 header_field complete
523+
off=46 len=7 span[header_value]="chunked"
524+
off=55 header_value complete
525+
off=57 headers complete status=304 v=1/1 flags=208 content_length=0
526+
off=57 message complete
527+
off=57 reset
528+
off=57 message begin
529+
off=62 len=3 span[version]="1.1"
530+
off=65 version complete
531+
off=70 len=2 span[status]="OK"
532+
off=74 status complete
533+
off=74 len=17 span[header_field]="Transfer-Encoding"
534+
off=92 header_field complete
535+
off=93 len=7 span[header_value]="chunked"
536+
off=102 header_value complete
537+
off=104 headers complete status=200 v=1/1 flags=208 content_length=0
538+
off=107 chunk header len=5
539+
off=107 len=5 span[body]="hello"
540+
off=114 chunk complete
541+
off=117 chunk header len=0
542+
```

test/response/transfer-encoding.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ off=61 header_field complete
3535
off=62 len=7 span[header_value]="chunked"
3636
off=71 header_value complete
3737
off=73 headers complete status=200 v=1/1 flags=208 content_length=0
38-
off=75 error code=12 reason="Invalid character in chunk size"
38+
off=76 error code=12 reason="Invalid character in chunk size"
3939
```
4040

4141
## `chunked` before other transfer-encoding
@@ -229,7 +229,7 @@ off=52 header_field complete
229229
off=53 len=7 span[header_value]="chunked"
230230
off=62 header_value complete
231231
off=64 headers complete status=200 v=1/1 flags=208 content_length=0
232-
off=65 error code=12 reason="Invalid character in chunk size"
232+
off=66 error code=12 reason="Invalid character in chunk size"
233233
```
234234

235235

0 commit comments

Comments
 (0)