diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index dd64c623..301ca9c7 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -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, @@ -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 = { @@ -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 = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index dbc9c607..441ea867 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -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; @@ -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. * @@ -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(name: string | T): T { diff --git a/src/native/api.c b/src/native/api.c index ae5e862d..e0a56edd 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -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 */ diff --git a/src/native/api.h b/src/native/api.h index 0a58d4e0..8781ae4b 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -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 diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 5f14cb2f..dcac7a78 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -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); diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index ec5b57d5..9a3e0a44 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -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[] = [ @@ -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'); diff --git a/test/md-test.ts b/test/md-test.ts index 5bdf2dce..c0f7ff84 100644 --- a/test/md-test.ts +++ b/test/md-test.ts @@ -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'); diff --git a/test/request/lenient-host-header.md b/test/request/lenient-host-header.md new file mode 100644 index 00000000..2e31e920 --- /dev/null +++ b/test/request/lenient-host-header.md @@ -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. + + + +```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. + + + +```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 +```