From 31926c06a2109b2c24f34943add3fa479602c437 Mon Sep 17 00:00:00 2001 From: Sujay Patel Date: Fri, 28 Aug 2026 10:16:43 -0700 Subject: [PATCH] feat: add lenient flag for duplicate Content-Length llhttp rejects a repeated `Content-Length` outright. RFC 9112 Section 6.3 lets a recipient either reject such a message or, when every value is identical, collapse them into one -- the latter arises when an upstream message processor combines or regenerates the header. This adds `llhttp_set_lenient_duplicate_content_length()`, off by default, alongside the existing lenient flags. With the flag set, the repeated header is routed to the general header-value path rather than erroring: `on_header_field` and `on_header_value` fire for every occurrence, and the body is framed with the first value. The parser deliberately does not compare the values. Doing so in the state machine would need a new property plus a custom native, since `isEqual` only compares a property to a constant, and callers that want this flag generally have the assembled header set already. The flag is documented accordingly: a caller enabling it MUST reject the message itself when the values differ. This is narrower than the two adjacent flags: LENIENT_CHUNKED_LENGTH and LENIENT_TRANSFER_ENCODING both permit what the spec forbids, whereas duplicate Content-Length with identical values is explicitly one of two conformant choices. Motivation: proxygen is migrating its HTTP/1.x codec from http_parser to llhttp. Its codec already accepts identical duplicates and rejects differing ones, and real-world traffic relies on that behaviour. It is an existing consumer of this mechanism, enabling lenient_chunked_length, optional_cr_before_lf and optional_lf_after_cr. --- README.md | 15 +++++++++++++++ src/llhttp/constants.ts | 1 + src/llhttp/http.ts | 4 +++- src/native/api.c | 8 ++++++++ src/native/api.h | 15 +++++++++++++++ test/fixtures/extra.c | 5 +++++ test/fixtures/index.ts | 2 ++ test/request/content-length.md | 34 ++++++++++++++++++++++++++++++++++ 8 files changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 019a1d15..390a59ed 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,21 @@ With this flag the extra value will be parsed normally. **Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!** +### `void llhttp_set_lenient_duplicate_content_length(llhttp_t* parser, int enabled)` + +Enables/disables lenient handling of a repeated `Content-Length` header. + +Normally `llhttp` would error when `Content-Length` appears more than once. + +This is important to prevent request smuggling, but RFC 9112 Section 6.3 also +permits collapsing duplicates whose values are identical. + +With this flag the repeat is parsed as an ordinary header and the body is framed +using the first value. llhttp does **not** compare the values; the caller must +reject the message itself when they differ. + +**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks unless you compare the values. USE WITH CAUTION!** + ### `void llhttp_set_lenient_version(llhttp_t* parser, int enabled)` Enables/disables lenient handling of HTTP version. diff --git a/src/llhttp/constants.ts b/src/llhttp/constants.ts index f500c83e..8b04400c 100644 --- a/src/llhttp/constants.ts +++ b/src/llhttp/constants.ts @@ -80,6 +80,7 @@ export const LENIENT_FLAGS = { OPTIONAL_CR_BEFORE_LF: 1 << 8, SPACES_AFTER_CHUNK_SIZE: 1 << 9, HEADER_VALUE_RELAXED: 1 << 10, + DUPLICATE_CONTENT_LENGTH: 1 << 11, } as const; export const STATUSES = { diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index 2f49e30b..7fd73d39 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -775,7 +775,9 @@ export class HTTP { n('header_value_content_length_once') .otherwise(this.testFlags(FLAGS.CONTENT_LENGTH, { 0: n('header_value_content_length'), - }, p.error(ERROR.UNEXPECTED_CONTENT_LENGTH, 'Duplicate Content-Length'))); + }, this.testLenientFlags(LENIENT_FLAGS.DUPLICATE_CONTENT_LENGTH, { + 1: fallback, + }, p.error(ERROR.UNEXPECTED_CONTENT_LENGTH, 'Duplicate Content-Length')))); n('header_value_content_length') .select(NUM_MAP, this.mulAdd('content_length', { diff --git a/src/native/api.c b/src/native/api.c index ae5e862d..35206b30 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -268,6 +268,14 @@ void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled) { } } +void llhttp_set_lenient_duplicate_content_length(llhttp_t* parser, int enabled) { + if (enabled) { + parser->lenient_flags |= LENIENT_DUPLICATE_CONTENT_LENGTH; + } else { + parser->lenient_flags &= ~LENIENT_DUPLICATE_CONTENT_LENGTH; + } +} + void llhttp_set_lenient_version(llhttp_t* parser, int enabled) { if (enabled) { parser->lenient_flags |= LENIENT_VERSION; diff --git a/src/native/api.h b/src/native/api.h index 0a58d4e0..30a05419 100644 --- a/src/native/api.h +++ b/src/native/api.h @@ -279,6 +279,21 @@ void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled); LLHTTP_EXPORT void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled); +/* Enables/disables lenient handling of a repeated `Content-Length` header. + * + * Normally `llhttp` would error when `Content-Length` appears more than once. + * This is important to prevent request smuggling, but RFC 9112 Section 6.3 also + * permits collapsing duplicates whose values are identical. + * With this flag the repeat is parsed as an ordinary header and the body is + * framed using the first value. llhttp does not compare the values; the caller + * must reject the message itself when they differ. + * + * **Enabling this flag can pose a security issue since you will be exposed to + * request smuggling attacks unless you compare the values. USE WITH CAUTION!** + */ +LLHTTP_EXPORT +void llhttp_set_lenient_duplicate_content_length(llhttp_t* parser, int enabled); + /* Enables/disables lenient handling of HTTP version. * * Normally `llhttp` would error when the HTTP version in the request or status line diff --git a/test/fixtures/extra.c b/test/fixtures/extra.c index 5f14cb2f..54f04fe5 100644 --- a/test/fixtures/extra.c +++ b/test/fixtures/extra.c @@ -118,6 +118,11 @@ void llhttp__test_init_request_lenient_transfer_encoding(llparse_t* s) { s->lenient_flags |= LENIENT_TRANSFER_ENCODING; } +void llhttp__test_init_request_lenient_duplicate_content_length(llparse_t* s) { + llhttp__test_init_request(s); + s->lenient_flags |= LENIENT_DUPLICATE_CONTENT_LENGTH; +} + void llhttp__test_init_request_lenient_version(llparse_t* s) { llhttp__test_init_request(s); diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index ec5b57d5..ee70c2af 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -16,6 +16,7 @@ export type TestType = 'request' | 'response' | 'request-finish' | 'response-fin 'request-lenient-all' | 'response-lenient-all' | 'request-lenient-headers' | 'response-lenient-headers' | 'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' | + 'request-lenient-duplicate-content-length' | 'request-lenient-keep-alive' | 'response-lenient-keep-alive' | 'request-lenient-version' | 'response-lenient-version' | 'request-lenient-data-after-close' | 'response-lenient-data-after-close' | @@ -39,6 +40,7 @@ export const allowedTypes: TestType[] = [ 'response-lenient-keep-alive', 'request-lenient-chunked-length', 'request-lenient-transfer-encoding', + 'request-lenient-duplicate-content-length', 'request-lenient-version', 'response-lenient-version', 'request-lenient-data-after-close', diff --git a/test/request/content-length.md b/test/request/content-length.md index 8317b056..ad2d7aa3 100644 --- a/test/request/content-length.md +++ b/test/request/content-length.md @@ -126,6 +126,40 @@ off=53 header_field complete off=54 error code=4 reason="Duplicate Content-Length" ``` +## Lenient duplicate `Content-Length` with identical values + + +```http +PUT /url HTTP/1.1 +Content-Length: 3 +Content-Length: 3 + +abc +``` + +```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=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=14 span[header_field]="Content-Length" +off=34 header_field complete +off=35 len=1 span[header_value]="3" +off=38 header_value complete +off=38 len=14 span[header_field]="Content-Length" +off=53 header_field complete +off=54 len=1 span[header_value]="3" +off=57 header_value complete +off=59 headers complete method=4 v=1/1 flags=20 content_length=3 +off=59 len=3 span[body]="abc" +off=62 message complete +``` + ## Error on simultaneous `Content-Length` and `Transfer-Encoding: identity`