From 2090cc34ab15c1e044a00748dd728ba69012d34a Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 7 Apr 2026 23:26:03 -0500 Subject: [PATCH 1/5] add lentient flag that probitings multiple host headers or none at all. --- src/llhttp/constants.ts | 4 ++++ src/llhttp/http.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index dd64c623..ae99b521 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_RELAXED: 1 << 11, } as const; export const STATUSES = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index dbc9c607..db27555e 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 not set to HOST_RELAXED + // This will reject repetative versions of the host header + const p = this.llparse; + return this.testLenientFlags( + ~LENIENT_FLAGS.HOST_RELAXED, + {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_RELAXED, + {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 { From c903dff2a3e02b6b8814e67280c37f2f3ca9734e Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 7 Apr 2026 23:27:28 -0500 Subject: [PATCH 2/5] add lentient flag that probiting multiple host headers or none at all. --- src/native/api.c | 8 ++++++++ src/native/api.h | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/src/native/api.c b/src/native/api.c index ae5e862d..e168cfc0 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_relaxed(llhttp_t* parser, int enabled) { + if (enabled) { + parser->lenient_flags |= LENIENT_HOST_RELAXED; + } else { + parser->lenient_flags &= ~LENIENT_HOST_RELAXED; + } +} + /* Callbacks */ diff --git a/src/native/api.h b/src/native/api.h index 0a58d4e0..816b8629 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_relaxed(llhttp_t* parser, int enabled); + + #ifdef __cplusplus } /* extern "C" */ #endif From b68aea9abec593dca5bc6c0494e427612d595c77 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 10:19:17 -0500 Subject: [PATCH 3/5] add lentient-host-header test and rename HOST lentient flag --- src/llhttp/constants.ts | 2 +- src/llhttp/http.ts | 6 +++--- src/native/api.c | 6 +++--- src/native/api.h | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index ae99b521..301ca9c7 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -83,7 +83,7 @@ export const LENIENT_FLAGS = { OPTIONAL_CR_BEFORE_LF: 1 << 8, SPACES_AFTER_CHUNK_SIZE: 1 << 9, HEADER_VALUE_RELAXED: 1 << 10, - HOST_RELAXED: 1 << 11, + HOST_HEADER: 1 << 11, } as const; export const STATUSES = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index db27555e..89d8f9c1 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -551,11 +551,11 @@ export class HTTP { } private buildHostCheck(next: Node): Node { - // Check if the lentient flag given is not set to HOST_RELAXED + // Check if the lentient flag given is not set to HOST_HEADER // This will reject repetative versions of the host header const p = this.llparse; return this.testLenientFlags( - ~LENIENT_FLAGS.HOST_RELAXED, + ~LENIENT_FLAGS.HOST_HEADER, {1: next}, this.testFlags( FLAGS.HOST_SEEN, @@ -1189,7 +1189,7 @@ export class HTTP { // 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_RELAXED, + ~LENIENT_FLAGS.HOST_HEADER, {1:this.testFlags( FLAGS.HOST_SEEN, {1: beforeHeadersComplete}, diff --git a/src/native/api.c b/src/native/api.c index e168cfc0..e0a56edd 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -324,11 +324,11 @@ void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled) { } } -void llhttp_set_lenient_host_relaxed(llhttp_t* parser, int enabled) { +void llhttp_set_lenient_host_header(llhttp_t* parser, int enabled) { if (enabled) { - parser->lenient_flags |= LENIENT_HOST_RELAXED; + parser->lenient_flags |= LENIENT_HOST_HEADER; } else { - parser->lenient_flags &= ~LENIENT_HOST_RELAXED; + parser->lenient_flags &= ~LENIENT_HOST_HEADER; } } diff --git a/src/native/api.h b/src/native/api.h index 816b8629..8781ae4b 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -376,7 +376,7 @@ void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled); * from being accepted. */ LLHTTP_EXPORT -void llhttp_set_lenient_host_relaxed(llhttp_t* parser, int enabled); +void llhttp_set_lenient_host_header(llhttp_t* parser, int enabled); #ifdef __cplusplus From c168e8ed00b83eb4a82b0e5cd5861ca12e5c56f4 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 10:25:58 -0500 Subject: [PATCH 4/5] add tests and run linter --- test/fixtures/extra.c | 6 ++++++ test/request/lenitent-host-header.md | 31 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/request/lenitent-host-header.md diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 5f14cb2f..92fb7ab3 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_relaxed(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/request/lenitent-host-header.md b/test/request/lenitent-host-header.md new file mode 100644 index 00000000..cd32768c --- /dev/null +++ b/test/request/lenitent-host-header.md @@ -0,0 +1,31 @@ +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) +HOST_HEADER if set should allow multiple hosts to be set. + + + +```http +GET /url HTTP/1.1 +host: www.python.org +host: llhttp.org + +``` + + +## Multiple Host + +HOST_HEADER if disabled should not allow multiple headers to be set. + +```http +GET /url HTTP/1.1 +host: www.python.org +host: llhttp.org + +``` + From 66b6f10474ac45ce9a9e96eaca3f8e8f48a22757 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 12:22:31 -0500 Subject: [PATCH 5/5] add lenitent-host-header test --- src/llhttp/http.ts | 8 ++-- test/fixtures/extra.c | 2 +- test/fixtures/index.ts | 2 + test/md-test.ts | 2 + test/request/lenient-host-header.md | 64 ++++++++++++++++++++++++++++ test/request/lenitent-host-header.md | 31 -------------- 6 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 test/request/lenient-host-header.md delete mode 100644 test/request/lenitent-host-header.md diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 89d8f9c1..441ea867 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -551,12 +551,12 @@ export class HTTP { } private buildHostCheck(next: Node): Node { - // Check if the lentient flag given is not set to HOST_HEADER + // 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}, + LENIENT_FLAGS.HOST_HEADER, + {1: next}, this.testFlags( FLAGS.HOST_SEEN, {1: p.error(ERROR.HOST_PREVIOUSLY_SEEN, "host provided multiple times.")}, @@ -1189,7 +1189,7 @@ export class HTTP { // 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, + LENIENT_FLAGS.HOST_HEADER, {1:this.testFlags( FLAGS.HOST_SEEN, {1: beforeHeadersComplete}, diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 92fb7ab3..dcac7a78 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -201,7 +201,7 @@ 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_relaxed(llparse_t* s) { +void llhttp__test_init_request_lenient_host_header(llparse_t* s) { llhttp__test_init_request(s); s->lenient_flags |= LENIENT_HOST_HEADER; } 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 +``` diff --git a/test/request/lenitent-host-header.md b/test/request/lenitent-host-header.md deleted file mode 100644 index cd32768c..00000000 --- a/test/request/lenitent-host-header.md +++ /dev/null @@ -1,31 +0,0 @@ -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) -HOST_HEADER if set should allow multiple hosts to be set. - - - -```http -GET /url HTTP/1.1 -host: www.python.org -host: llhttp.org - -``` - - -## Multiple Host - -HOST_HEADER if disabled should not allow multiple headers to be set. - -```http -GET /url HTTP/1.1 -host: www.python.org -host: llhttp.org - -``` -