Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/llhttp/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const ERROR = {
INVALID_STATUS: 13,
INVALID_EOF_STATE: 14,
INVALID_TRANSFER_ENCODING: 15,
HOST_PREVIOUSLY_SEEN: 39,
HOST_NOT_PROVIDED: 40,

CB_MESSAGE_BEGIN: 16,
CB_HEADERS_COMPLETE: 17,
Expand Down Expand Up @@ -66,6 +68,7 @@ export const FLAGS = {
TRAILING: 1 << 7,
// 1 << 8 is unused
TRANSFER_ENCODING: 1 << 9,
HOST_SEEN: 1 << 10,
} as const;

export const LENIENT_FLAGS = {
Expand All @@ -80,6 +83,7 @@ export const LENIENT_FLAGS = {
OPTIONAL_CR_BEFORE_LF: 1 << 8,
SPACES_AFTER_CHUNK_SIZE: 1 << 9,
HEADER_VALUE_RELAXED: 1 << 10,
HOST_HEADER: 1 << 11,
} as const;

export const STATUSES = {
Expand Down
35 changes: 33 additions & 2 deletions src/llhttp/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,21 @@ export class HTTP {
this.buildHeaderValue();
}

private buildHostCheck(next: Node): Node {
// Check if the lentient flag given is set to HOST_HEADER
// This will reject repetative versions of the host header
const p = this.llparse;
return this.testLenientFlags(
LENIENT_FLAGS.HOST_HEADER,
{1: next},
this.testFlags(
FLAGS.HOST_SEEN,
{1: p.error(ERROR.HOST_PREVIOUSLY_SEEN, "host provided multiple times.")},
this.setFlag(FLAGS.HOST_SEEN, next),
)
);
}

private buildHeaderField(): void {
const p = this.llparse;
const span = this.span;
Expand Down Expand Up @@ -578,12 +593,18 @@ export class HTTP {
)
.peek(':', p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header token'))
.otherwise(span.headerField.start(n('header_field')));


const reset_header_state = this.resetHeaderState('header_field_general');

n('header_field')
.transform(p.transform.toLower())
// Match headers that need special treatment
.select(SPECIAL_HEADERS, this.store('header_state', 'header_field_colon'))
.otherwise(this.resetHeaderState('header_field_general'));
// check to see if host was given once or multiple times which if not
// relaxed should be easily rejected.
.match('host', this.buildHostCheck(reset_header_state))
.otherwise(reset_header_state);

/* https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3, paragraph 3.
*
Expand Down Expand Up @@ -1165,7 +1186,17 @@ export class HTTP {

beforeHeadersComplete.otherwise(onHeadersComplete);

return beforeHeadersComplete;
// before leaving header state if Host is not set to being
// relaxed see if no host has been provided at all...
return this.testLenientFlags(
LENIENT_FLAGS.HOST_HEADER,
{1:this.testFlags(
FLAGS.HOST_SEEN,
{1: beforeHeadersComplete},
p.error(ERROR.HOST_NOT_PROVIDED, "No host header or value was provided.")
)},
beforeHeadersComplete,
);
}

private node<T extends Node>(name: string | T): T {
Expand Down
8 changes: 8 additions & 0 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,14 @@ void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled) {
}
}

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

/* Callbacks */


Expand Down
9 changes: 9 additions & 0 deletions src/native/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,15 @@ void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled);
LLHTTP_EXPORT
void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled);


/* Enables/disables relaxed handling of the host header, which can allow multiple
* or no host headers, when disabled it strictly prohibits these form of requests
* from being accepted.
*/
LLHTTP_EXPORT
void llhttp_set_lenient_host_header(llhttp_t* parser, int enabled);


#ifdef __cplusplus
} /* extern "C" */
#endif
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ void llhttp__test_init_response_lenient_header_value_relaxed(llparse_t* s) {
s->lenient_flags |= LENIENT_HEADER_VALUE_RELAXED;
}

void llhttp__test_init_request_lenient_host_header(llparse_t* s) {
llhttp__test_init_request(s);
s->lenient_flags |= LENIENT_HOST_HEADER;
}



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

export const allowedTypes: TestType[] = [
Expand Down Expand Up @@ -53,6 +54,7 @@ export const allowedTypes: TestType[] = [
'response-lenient-spaces-after-chunk-size',
'request-lenient-header-value-relaxed',
'response-lenient-header-value-relaxed',
'request-lenient-host-header'
];

const BUILD_DIR = path.join(__dirname, '..', 'tmp');
Expand Down
2 changes: 2 additions & 0 deletions test/md-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,11 @@ function run(name: string): void {
}
}


run('request/sample');
run('request/lenient-headers');
run('request/lenient-header-value-relaxed');
run('request/lenient-host-header');
run('request/lenient-version');
run('request/method');
run('request/uri');
Expand Down
64 changes: 64 additions & 0 deletions test/request/lenient-host-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Relaxed host header
===================

Relaxed host header mode: accepts multiple host headers
this is meant to stop redirection or injection attacks
and other unusual behaviors.

## multiple host headers (relaxed)
When HOST_HEADER is not set, it should allow multiple hosts to be set.

<!-- meta={"type": "request"} -->

```http
GET / HTTP/1.1
host: www.python.org
host: llhttp.org


```
```log
off=0 message begin
off=0 len=3 span[method]="GET"
off=3 method complete
off=4 len=4 span[url]="/url"
off=9 url complete
off=9 len=4 span[protocol]="HTTP"
off=13 protocol complete
off=14 len=3 span[version]="1.1"
off=17 version complete
off=19 len=4 span[header_field] = "host"
off=24 len=10 span[header_value] = "www.python.org"
off=35 len=4 span[header_field] = "host"
off=40 len=10 span[header_field] = "llhttp.org"
```


## Invalid Hosts (strict)

HOST_HEADER if enabled this will not allow multiple headers to be set.

<!-- meta={"type": "request-lenient-host-header"} -->

```http
GET /url HTTP/1.1
host: www.python.org
host: llhttp.org


```

```log
off=0 message begin
off=0 len=3 span[method]="GET"
off=3 method complete
off=4 len=4 span[url]="/url"
off=9 url complete
off=9 len=4 span[protocol]="HTTP"
off=13 protocol complete
off=14 len=3 span[version]="1.1"
off=17 version complete
off=19 len=4 span[header_field] = "host"
off=24 len=10 span[header_value] = "www.python.org"
off=19 len=4 error code=40
```
Loading