Skip to content

Commit

Permalink
New lenient flag to have spaces after chunk header. (#245)
Browse files Browse the repository at this point in the history
* fix: Do not allow body for HTTP 304.

* feat: Added new lenient flags to allow spaces after chunk header.
  • Loading branch information
ShogunPanda authored Sep 13, 2023
1 parent 50524c0 commit 6922f5b
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 20 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ With this flag the new chunk can start immediately after the previous one.
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
### `void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)`
Enables/disables lenient handling of spaces after chunk size.
Normally `llhttp` would error when after a chunk size is followed by one or more spaces are present instead of a CRLF or `;`.
With this flag this check is disabled.
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
## Build Instructions
Make sure you have [Node.js](https://nodejs.org/), npm and npx installed. Then under project directory run:
Expand Down
1 change: 1 addition & 0 deletions src/llhttp/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export enum LENIENT_FLAGS {
OPTIONAL_LF_AFTER_CR = 1 << 6,
OPTIONAL_CRLF_AFTER_CHUNK = 1 << 7,
OPTIONAL_CR_BEFORE_LF = 1 << 8,
SPACES_AFTER_CHUNK_SIZE = 1 << 9,
}

export enum METHODS {
Expand Down
10 changes: 10 additions & 0 deletions src/llhttp/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,16 @@ export class HTTP {
.otherwise(n('chunk_size_otherwise'));

n('chunk_size_otherwise')
.match(
[ ' ', '\t' ],
this.testLenientFlags(
LENIENT_FLAGS.SPACES_AFTER_CHUNK_SIZE,
{
1: n('chunk_size_otherwise'),
},
p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size'),
),
)
.match('\r', n('chunk_size_almost_done'))
.match(
'\n',
Expand Down
8 changes: 8 additions & 0 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) {
}
}

void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
} else {
parser->lenient_flags &= ~LENIENT_SPACES_AFTER_CHUNK_SIZE;
}
}

/* Callbacks */


Expand Down
19 changes: 16 additions & 3 deletions src/native/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,26 @@ int llhttp__after_headers_complete(llhttp_t* parser, const char* p,
int hasBody;

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

if (parser->flags & F_SKIPBODY) {
/* See RFC 2616 section 4.4 */
if (
parser->flags & F_SKIPBODY || /* response to a HEAD request */
(
parser->type == HTTP_RESPONSE && (
parser->status_code == 204 || /* No Content */
parser->status_code == 304 /* Not Modified */
)
)
) {
return 0;
} else if (parser->flags & F_CHUNKED) {
/* chunked encoding - ignore Content-Length header, prepare for a chunk */
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ void llhttp__test_init_response_lenient_optional_crlf_after_chunk(llparse_t* s)
s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
}

void llhttp__test_init_request_lenient_spaces_after_chunk_size(llparse_t* s) {
llhttp__test_init_request(s);
s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
}

void llhttp__test_init_response_lenient_spaces_after_chunk_size(llparse_t* s) {
llhttp__test_init_response(s);
s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
}


void llhttp__test_finish(llparse_t* s) {
llparse__print(NULL, NULL, "finish=%d", s->finish);
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type TestType = 'request' | 'response' | 'request-finish' | 'response-fin
'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' |
'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' |
'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' |
'request-lenient-spaces-after-chunk-size' | 'response-lenient-spaces-after-chunk-size' |
'none' | 'url';

export const allowedTypes: TestType[] = [
Expand All @@ -45,6 +46,8 @@ export const allowedTypes: TestType[] = [
'response-lenient-optional-cr-before-lf',
'request-lenient-optional-crlf-after-chunk',
'response-lenient-optional-crlf-after-chunk',
'request-lenient-spaces-after-chunk-size',
'response-lenient-spaces-after-chunk-size',
];

const BUILD_DIR = path.join(__dirname, '..', 'tmp');
Expand Down
67 changes: 65 additions & 2 deletions test/request/transfer-encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ off=83 header_field complete
off=84 len=7 span[header_value]="chunked"
off=93 header_value complete
off=95 headers complete method=3 v=1/1 flags=208 content_length=0
off=96 error code=12 reason="Invalid character in chunk size"
off=97 error code=12 reason="Invalid character in chunk size"
```

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

## Invalid OBS fold after chunked value
Expand Down Expand Up @@ -1117,4 +1117,67 @@ off=79 chunk header len=5
off=79 len=5 span[body]="ABCDE"
off=84 chunk complete
off=87 chunk header len=0
```

## Space after chunk header

<!-- meta={"type": "request"} -->
```http
PUT /url HTTP/1.1
Transfer-Encoding: chunked
a \r\n0123456789
0
```

```log
off=0 message begin
off=0 len=3 span[method]="PUT"
off=3 method complete
off=4 len=4 span[url]="/url"
off=9 url complete
off=14 len=3 span[version]="1.1"
off=17 version complete
off=19 len=17 span[header_field]="Transfer-Encoding"
off=37 header_field complete
off=38 len=7 span[header_value]="chunked"
off=47 header_value complete
off=49 headers complete method=4 v=1/1 flags=208 content_length=0
off=51 error code=12 reason="Invalid character in chunk size"
```

## Space after chunk header (lenient)

<!-- meta={"type": "request-lenient-spaces-after-chunk-size"} -->
```http
PUT /url HTTP/1.1
Transfer-Encoding: chunked
a \r\n0123456789
0
```

```log
off=0 message begin
off=0 len=3 span[method]="PUT"
off=3 method complete
off=4 len=4 span[url]="/url"
off=9 url complete
off=14 len=3 span[version]="1.1"
off=17 version complete
off=19 len=17 span[header_field]="Transfer-Encoding"
off=37 header_field complete
off=38 len=7 span[header_value]="chunked"
off=47 header_value complete
off=49 headers complete method=4 v=1/1 flags=208 content_length=0
off=53 chunk header len=10
off=53 len=10 span[body]="0123456789"
off=65 chunk complete
off=68 chunk header len=0
off=70 chunk complete
off=70 message complete
```
103 changes: 90 additions & 13 deletions test/response/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,8 @@ off=84 header_field complete
off=85 len=1 span[header_value]="4"
off=88 header_value complete
off=90 headers complete status=101 v=1/1 flags=34 content_length=4
off=90 len=4 span[body]="body"
off=94 message complete
off=94 error code=22 reason="Pause on CONNECT/Upgrade"
off=90 message complete
off=90 error code=22 reason="Pause on CONNECT/Upgrade"
```

## HTTP 101 response with Upgrade and Transfer-Encoding header
Expand Down Expand Up @@ -340,16 +339,8 @@ off=87 header_field complete
off=88 len=7 span[header_value]="chunked"
off=97 header_value complete
off=99 headers complete status=101 v=1/1 flags=21c content_length=0
off=102 chunk header len=2
off=102 len=2 span[body]="bo"
off=106 chunk complete
off=109 chunk header len=2
off=109 len=2 span[body]="dy"
off=113 chunk complete
off=116 chunk header len=0
off=118 chunk complete
off=118 message complete
off=118 error code=22 reason="Pause on CONNECT/Upgrade"
off=99 message complete
off=99 error code=22 reason="Pause on CONNECT/Upgrade"
```

## HTTP 200 response with Upgrade header
Expand Down Expand Up @@ -463,3 +454,89 @@ off=99 chunk header len=0
off=101 chunk complete
off=101 message complete
```

## HTTP 304 with Content-Length

<!-- meta={"type": "response"} -->
```http
HTTP/1.1 304 Not Modified
Content-Length: 10
HTTP/1.1 200 OK
Content-Length: 5
hello
```

```log
off=0 message begin
off=5 len=3 span[version]="1.1"
off=8 version complete
off=13 len=12 span[status]="Not Modified"
off=27 status complete
off=27 len=14 span[header_field]="Content-Length"
off=42 header_field complete
off=43 len=2 span[header_value]="10"
off=47 header_value complete
off=49 headers complete status=304 v=1/1 flags=20 content_length=10
off=49 message complete
off=51 reset
off=51 message begin
off=56 len=3 span[version]="1.1"
off=59 version complete
off=64 len=2 span[status]="OK"
off=68 status complete
off=68 len=14 span[header_field]="Content-Length"
off=83 header_field complete
off=84 len=1 span[header_value]="5"
off=87 header_value complete
off=89 headers complete status=200 v=1/1 flags=20 content_length=5
off=89 len=5 span[body]="hello"
off=94 message complete
```

## HTTP 304 with Transfer-Encoding

<!-- meta={"type": "response"} -->
```http
HTTP/1.1 304 Not Modified
Transfer-Encoding: chunked
HTTP/1.1 200 OK
Transfer-Encoding: chunked
5
hello
0
```

```log
off=0 message begin
off=5 len=3 span[version]="1.1"
off=8 version complete
off=13 len=12 span[status]="Not Modified"
off=27 status complete
off=27 len=17 span[header_field]="Transfer-Encoding"
off=45 header_field complete
off=46 len=7 span[header_value]="chunked"
off=55 header_value complete
off=57 headers complete status=304 v=1/1 flags=208 content_length=0
off=57 message complete
off=57 reset
off=57 message begin
off=62 len=3 span[version]="1.1"
off=65 version complete
off=70 len=2 span[status]="OK"
off=74 status complete
off=74 len=17 span[header_field]="Transfer-Encoding"
off=92 header_field complete
off=93 len=7 span[header_value]="chunked"
off=102 header_value complete
off=104 headers complete status=200 v=1/1 flags=208 content_length=0
off=107 chunk header len=5
off=107 len=5 span[body]="hello"
off=114 chunk complete
off=117 chunk header len=0
```
4 changes: 2 additions & 2 deletions test/response/transfer-encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ off=61 header_field complete
off=62 len=7 span[header_value]="chunked"
off=71 header_value complete
off=73 headers complete status=200 v=1/1 flags=208 content_length=0
off=75 error code=12 reason="Invalid character in chunk size"
off=76 error code=12 reason="Invalid character in chunk size"
```

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


Expand Down

0 comments on commit 6922f5b

Please sign in to comment.