From 4d4baab72824ab86e2015eea913b1b6ff8aa8bac Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 21 Mar 2026 20:59:10 -0400 Subject: [PATCH 01/11] HTTP/1.x: Strip leading and trailing tabs from field values The code previously allowed them, but RFC9110 is clear that field values never start or end with a tab. --- src/http/ngx_http_parse.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 81f689e5b..51f26e8de 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -1013,6 +1013,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, case sw_space_before_value: switch (ch) { case ' ': + case '\t': break; case CR: r->header_start = p; @@ -1037,6 +1038,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, case sw_value: switch (ch) { case ' ': + case '\t': r->header_end = p; state = sw_space_after_value; break; @@ -1057,6 +1059,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, case sw_space_after_value: switch (ch) { case ' ': + case '\t': break; case CR: state = sw_almost_done; From ce56075796a33df8e7be6953b409ffe899c80156 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Thu, 26 Mar 2026 02:34:02 -0400 Subject: [PATCH 02/11] HTTP/1.x: Remove support for duplicate response lines Presumably only ancient versions of IIS send them. Signed-off-by: Demi Marie Obenour --- src/http/ngx_http_parse.c | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 51f26e8de..704c3f283 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -865,7 +865,6 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, sw_space_before_value, sw_value, sw_space_after_value, - sw_ignore_line, sw_almost_done, sw_header_almost_done } state; @@ -990,16 +989,6 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, goto done; } - /* IIS may send the duplicate "HTTP/1.1 ..." lines */ - if (ch == '/' - && r->upstream - && p - r->header_name_start == 4 - && ngx_strncmp(r->header_name_start, "HTTP", 4) == 0) - { - state = sw_ignore_line; - break; - } - if (ch <= 0x20 || ch == 0x7f) { r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; @@ -1075,17 +1064,6 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, } break; - /* ignore header line */ - case sw_ignore_line: - switch (ch) { - case LF: - state = sw_start; - break; - default: - break; - } - break; - /* end of header line */ case sw_almost_done: switch (ch) { From ff7479f8b16c0b8ce2dff54fb595510a64c8736a Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 25 Apr 2025 02:17:29 -0400 Subject: [PATCH 03/11] HTTP/1.x: Do not allow header lines with no colon RFC9112 does not permit them, and no other HTTP parser I know of allows them. --- src/http/ngx_http_parse.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 704c3f283..2e04df953 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -974,21 +974,6 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, break; } - if (ch == CR) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - state = sw_almost_done; - break; - } - - if (ch == LF) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - goto done; - } - if (ch <= 0x20 || ch == 0x7f) { r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; From 308168f3b187448a687358b416ccf1b5bec11753 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 14 Mar 2025 01:00:59 -0400 Subject: [PATCH 04/11] HTTP/1.x: Reject invalid header names HTTP headers must be an RFC9110 token, so only a subset of characters are permitted. RFC9113 and RFC9114 require rejecting invalid header characters in HTTP/2 and HTTP/3 respectively, so reject them in HTTP/1.0 and HTTP/1.1 for consistency. This also requires removing the ignore hack for (presumably ancient) versions of IIS. --- src/http/ngx_http_parse.c | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 2e04df953..79f37ec6b 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -852,6 +852,29 @@ done: return NGX_OK; } +static ngx_int_t +ngx_http_non_alnum_dash_header_char(u_char ch) +{ + switch (ch) { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return 1; + default: + return 0; + } +} ngx_int_t ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, @@ -915,22 +938,14 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, break; } - if (ch == '_') { - if (allow_underscores) { - hash = ngx_hash(0, ch); - r->lowcase_header[0] = ch; - i = 1; - - } else { - hash = 0; - i = 0; - r->invalid_header = 1; - } - + if (ch == '_' && allow_underscores) { + hash = ngx_hash(0, ch); + r->lowcase_header[0] = ch; + i = 1; break; } - if (ch <= 0x20 || ch == 0x7f || ch == ':') { + if (!ngx_http_non_alnum_dash_header_char(ch)) { r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; } @@ -974,7 +989,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, break; } - if (ch <= 0x20 || ch == 0x7f) { + if (!ngx_http_non_alnum_dash_header_char(ch)) { r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; } From 4fb051018fb04ba1db4f6c176b99624bff9e69ce Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 9 Apr 2025 01:30:30 -0400 Subject: [PATCH 05/11] HTTP/1.x: Reject invalid field values RFC9110 is clear that the only CTRL character allowed in header values is HTAB. Conform to the standard, as Varnish, H2O, and (I suspect) Hyper do. --- src/http/ngx_http_parse.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 79f37ec6b..10db1a083 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -1013,13 +1013,14 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, r->header_start = p; r->header_end = p; goto done; - case '\0': + default: + if (ch > 0x20 && ch != 0x7f) { + r->header_start = p; + state = sw_value; + break; + } r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; - default: - r->header_start = p; - state = sw_value; - break; } break; @@ -1038,7 +1039,9 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, case LF: r->header_end = p; goto done; - case '\0': + default: + if (ch > 0x20 && ch != 0x7f) + break; r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; } @@ -1055,12 +1058,13 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, break; case LF: goto done; - case '\0': + default: + if (ch > 0x20 && ch != 0x7f) { + state = sw_value; + break; + } r->header_end = p; return NGX_HTTP_PARSE_INVALID_HEADER; - default: - state = sw_value; - break; } break; From 20a43a1c2b101102c37de81ece886e96f1e363df Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 14 Mar 2025 02:10:46 -0400 Subject: [PATCH 06/11] HTTP/1.x: Do not allow multiple CRs before LF This is not permitted by RFC9112. --- src/http/ngx_http_parse.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 10db1a083..c848ef2e3 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -1073,8 +1073,6 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, switch (ch) { case LF: goto done; - case CR: - break; default: return NGX_HTTP_PARSE_INVALID_HEADER; } From 381c88494a836e49d2963ee47631030ffbc8c690 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sun, 23 Mar 2025 16:18:19 -0400 Subject: [PATCH 07/11] HTTP/1.1: Reject malformed chunk extensions This forbids chunk extensions that violate RFC9112, and _only_ these chunk extensions. Bad whitespace is permitted. --- src/http/ngx_http_parse.c | 200 ++++++++++++++++++++++++++++---------- 1 file changed, 147 insertions(+), 53 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index c848ef2e3..3bb730b01 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -101,6 +101,11 @@ static uint32_t usual[] = { #endif +static inline ngx_int_t +ngx_http_field_value_char(u_char ch) +{ + return ch >= 0x20 ? ch != 0x7f : ch == 0x09; +} /* gcc, icc, msvc and others compile these switches as an jump table */ @@ -876,6 +881,17 @@ ngx_http_non_alnum_dash_header_char(u_char ch) } } +static ngx_int_t +ngx_http_token_char(u_char ch) +{ + u_char c = (ch | 0x20); + if (('a' <= c && c <= 'z') || ('0' <= ch && ch <= '9') || ch == '-') { + return 1; + } + + return ngx_http_non_alnum_dash_header_char(ch); +} + ngx_int_t ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, ngx_uint_t allow_underscores) @@ -1824,6 +1840,11 @@ ngx_http_parse_status_line(ngx_http_request_t *r, ngx_buf_t *b, break; case LF: goto done; + default: + if (ch < 0x20 || ch == 0x7f) { + return NGX_ERROR; + } + break; } break; @@ -2189,13 +2210,19 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, enum { sw_chunk_start = 0, sw_chunk_size, + sw_chunk_extension_before_semi, + sw_chunk_extension_before_semi_bws, sw_chunk_extension, + sw_chunk_extension_bws_before_equal, + sw_chunk_extension_name, + sw_chunk_extension_value_start, + sw_chunk_extension_quoted_value, + sw_chunk_extension_value_quoted_backslash, + sw_chunk_extension_unquoted_value, sw_chunk_extension_almost_done, sw_chunk_data, sw_after_data, sw_after_data_almost_done, - sw_last_chunk_extension, - sw_last_chunk_extension_almost_done, sw_trailer, sw_trailer_almost_done, sw_trailer_header, @@ -2252,56 +2279,53 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, ctx->size = ctx->size * 16 + (c - 'a' + 10); break; } + /* fall through */ - if (ctx->size == 0) { - - switch (ch) { - case CR: - state = sw_last_chunk_extension_almost_done; - break; - case ';': - case ' ': - case '\t': - state = sw_last_chunk_extension; - break; - default: - goto invalid; - } - - break; - } - + case sw_chunk_extension_before_semi: +before_semi: switch (ch) { case CR: state = sw_chunk_extension_almost_done; break; case ';': - case ' ': - case '\t': state = sw_chunk_extension; break; + case ' ': + case '\t': + state = sw_chunk_extension_before_semi_bws; + break; /* BWS */ default: goto invalid; } break; - case sw_chunk_extension: + case sw_chunk_extension_before_semi_bws: switch (ch) { - case CR: - state = sw_chunk_extension_almost_done; + case ' ': + case '\t': break; - case LF: + case ';': + state = sw_chunk_extension; + break; + default: goto invalid; } break; - case sw_chunk_extension_almost_done: - if (ch == LF) { - state = sw_chunk_data; + case sw_chunk_extension: + if (ngx_http_token_char(ch)) { + state = sw_chunk_extension_name; break; } - goto invalid; + switch (ch) { + case ' ': + case '\t': + break; /* BWS */ + default: + goto invalid; + } + break; case sw_chunk_data: rc = NGX_OK; @@ -2324,18 +2348,78 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, } goto invalid; - case sw_last_chunk_extension: - switch (ch) { - case CR: - state = sw_last_chunk_extension_almost_done; + case sw_chunk_extension_name: + if (ngx_http_token_char(ch)) { break; - case LF: + } + + state = sw_chunk_extension_bws_before_equal; + + /* fall through */ + + case sw_chunk_extension_bws_before_equal: + switch (ch) { + case ' ': + case '\t': + break; /* BWS */ + case '=': + state = sw_chunk_extension_value_start; + break; + default: goto invalid; } break; - case sw_last_chunk_extension_almost_done: + case sw_chunk_extension_value_start: + if (ngx_http_token_char(ch)) { + state = sw_chunk_extension_unquoted_value; + break; + } + switch (ch) { + case ' ': + case '\t': + break; /* BWS */ + case '"': + state = sw_chunk_extension_quoted_value; + break; + default: + goto invalid; + } + break; + + case sw_chunk_extension_quoted_value: + if (ch == '"') { + state = sw_chunk_extension_before_semi; + break; + } + if (ch == '\\') { + state = sw_chunk_extension_value_quoted_backslash; + break; + } + if (ngx_http_field_value_char(ch)) { + break; + } + goto invalid; + + case sw_chunk_extension_value_quoted_backslash: + if (ngx_http_field_value_char(ch)) { + state = sw_chunk_extension_quoted_value; + break; + } + goto invalid; + + case sw_chunk_extension_unquoted_value: + if (ngx_http_token_char(ch)) { + break; + } + goto before_semi; + + case sw_chunk_extension_almost_done: if (ch == LF) { + if (ctx->size) { + state = sw_chunk_data; + break; + } if (keep_trailers) { goto done; } @@ -2387,28 +2471,44 @@ data: ctx->state = state; b->pos = pos; - if (ctx->size > NGX_MAX_OFF_T_VALUE - 5) { + if (ctx->size > NGX_MAX_OFF_T_VALUE - 13) { goto invalid; } - + off_t min_length = (ctx->size ? ctx->size + 7 /* CRLF "0" CRLF CRLF */ + : 2 /* CRLF */); switch (state) { - case sw_chunk_start: ctx->length = 5 /* "0" CRLF CRLF */; break; case sw_chunk_size: - ctx->length = 2 /* CRLF */ - + (ctx->size ? ctx->size + 7 /* CRLF "0" CRLF CRLF */ - : 2 /* CRLF */); - break; - case sw_chunk_extension: - ctx->length = 2 /* CRLF */ + ctx->size + 7 /* CRLF "0" CRLF CRLF */; + case sw_chunk_extension_before_semi: + case sw_chunk_extension_unquoted_value: + ctx->length = 2 /* CRLF */ + min_length; break; case sw_chunk_extension_almost_done: - ctx->length = 1 /* LF */ + ctx->size + 7 /* CRLF "0" CRLF CRLF */; + ctx->length = 1 /* LF */ + min_length; + break; + case sw_chunk_extension_before_semi_bws: + ctx->length = 6 /* ;a=b CRLF */ + min_length; + break; + case sw_chunk_extension: + ctx->length = 5 /* a=b CRLF */ + min_length; + break; + case sw_chunk_extension_bws_before_equal: + case sw_chunk_extension_name: + ctx->length = 4 /* =b CRLF */ + min_length; + break; + case sw_chunk_extension_value_start: + ctx->length = 3 /* b CRLF */ + min_length; + break; + case sw_chunk_extension_quoted_value: + ctx->length = 3 /* " CRLF */ + min_length; + break; + case sw_chunk_extension_value_quoted_backslash: + ctx->length = 4 /* a" CRLF */ + min_length; break; case sw_chunk_data: - ctx->length = ctx->size + 7 /* CRLF "0" CRLF CRLF */; + ctx->length = min_length; break; case sw_after_data: ctx->length = 7 /* CRLF "0" CRLF CRLF */; @@ -2416,12 +2516,6 @@ data: case sw_after_data_almost_done: ctx->length = 6 /* LF "0" CRLF CRLF */; break; - case sw_last_chunk_extension: - ctx->length = 4 /* CRLF CRLF */; - break; - case sw_last_chunk_extension_almost_done: - ctx->length = 3 /* LF CRLF */; - break; case sw_trailer: ctx->length = 2 /* CRLF */; break; From 0066543c08c838a9c724ac940a2b0435879c2dc4 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 7 Apr 2025 01:25:03 -0400 Subject: [PATCH 08/11] HTTP/1.x: Remove redundant state from chunk parsing The state machine never returns with state = sw_chunk_data and a size of zero, nor did it return with state = sw_after_data. --- src/http/ngx_http_parse.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index 3bb730b01..bbafbeaf7 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -2221,7 +2221,6 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, sw_chunk_extension_unquoted_value, sw_chunk_extension_almost_done, sw_chunk_data, - sw_after_data, sw_after_data_almost_done, sw_trailer, sw_trailer_almost_done, @@ -2231,10 +2230,6 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, state = ctx->state; - if (state == sw_chunk_data && ctx->size == 0) { - state = sw_after_data; - } - rc = NGX_AGAIN; for (pos = b->pos; pos < b->last; pos++) { @@ -2328,10 +2323,11 @@ before_semi: break; case sw_chunk_data: - rc = NGX_OK; - goto data; + if (ctx->size != 0) { + rc = NGX_OK; + goto data; + } - case sw_after_data: switch (ch) { case CR: state = sw_after_data_almost_done; @@ -2510,9 +2506,6 @@ data: case sw_chunk_data: ctx->length = min_length; break; - case sw_after_data: - ctx->length = 7 /* CRLF "0" CRLF CRLF */; - break; case sw_after_data_almost_done: ctx->length = 6 /* LF "0" CRLF CRLF */; break; From 9b230e33f7eae9a2f2ac4622a0374489940b864d Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 26 Mar 2025 23:13:32 -0400 Subject: [PATCH 09/11] HTTP/1.1: Forbid malformed trailers HTTP/1.1 trailers must follow the same syntax as HTTP headers. See RFC9112. --- src/http/ngx_http_parse.c | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index bbafbeaf7..f577c5468 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -2224,7 +2224,8 @@ ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b, sw_after_data_almost_done, sw_trailer, sw_trailer_almost_done, - sw_trailer_header, + sw_trailer_name, + sw_trailer_value, sw_trailer_header_almost_done } state; @@ -2344,6 +2345,7 @@ before_semi: } goto invalid; + case sw_chunk_extension_name: if (ngx_http_token_char(ch)) { break; @@ -2425,16 +2427,15 @@ before_semi: goto invalid; case sw_trailer: - switch (ch) { - case CR: + if (ch == CR) { state = sw_trailer_almost_done; break; - case LF: - goto invalid; - default: - state = sw_trailer_header; } - break; + if (ngx_http_token_char(ch)) { + state = sw_trailer_name; + break; + } + goto invalid; case sw_trailer_almost_done: if (ch == LF) { @@ -2442,15 +2443,25 @@ before_semi: } goto invalid; - case sw_trailer_header: - switch (ch) { - case CR: + case sw_trailer_name: + if (ngx_http_token_char(ch)) { + break; + } + if (ch == ':') { + state = sw_trailer_value; + break; + } + goto invalid; + + case sw_trailer_value: + if (ngx_http_field_value_char(ch)) { + break; + } + if (ch == CR) { state = sw_trailer_header_almost_done; break; - case LF: - goto invalid; } - break; + goto invalid; case sw_trailer_header_almost_done: if (ch == LF) { @@ -2512,15 +2523,18 @@ data: case sw_trailer: ctx->length = 2 /* CRLF */; break; - case sw_trailer_almost_done: - ctx->length = 1 /* LF */; + case sw_trailer_name: + ctx->length = 5 /* : CRLF CRLF */; break; - case sw_trailer_header: + case sw_trailer_value: ctx->length = 4 /* CRLF CRLF */; break; case sw_trailer_header_almost_done: ctx->length = 3 /* LF CRLF */; break; + case sw_trailer_almost_done: + ctx->length = 1 /* LF */; + break; } From 99a8082e90f116eee7979652efb15c70f6afa700 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 26 Mar 2025 22:33:54 -0400 Subject: [PATCH 10/11] Proxy: Reject trailers involved in framing RFC9112 forbids including Content-Length, Transfer-Encoding, or Upgrade in the trailer section. If they were (invalidly) folded into a header by downstream code, it would allow HTTP response splitting. --- src/http/modules/ngx_http_proxy_module.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c index 0b388b30f..a3831727c 100644 --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -2576,7 +2576,24 @@ ngx_http_proxy_process_trailer(ngx_http_request_t *r, ngx_buf_t *buf) if (rc == NGX_OK) { - /* a header line has been parsed successfully */ + /* A trailer line has been parsed successfully. + * Do not allow trailers that would, if turned into + * headers, interfere with request framing. */ + switch (r->header_name_end - r->header_name_start) { +#define X(x) \ + case sizeof(x "") - 1: \ + /* The size is always less than the number of bytes in \ + * the pre-casefolded area. */ \ + if (memcmp(r->lowcase_header, x, sizeof(x) - 1) == 0) { \ + return NGX_ERROR; \ + } else break + X("transfer-encoding"); + X("content-length"); + X("upgrade"); +#undef X + default: + break; + } h = ngx_list_push(&r->upstream->headers_in.trailers); if (h == NULL) { From 4de1b092fbaba5477a94bc7ac422e4fb67c7ced0 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 26 Mar 2025 23:34:29 -0400 Subject: [PATCH 11/11] HTTP: Reject trailers involved in framing RFC9112 forbids including Content-Length, Transfer-Encoding, or Upgrade in the trailer section. If they were (invalidly) folded into a header by upstream code, it would allow HTTP request smuggling. --- src/http/ngx_http_parse.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c index f577c5468..b4fc241d4 100644 --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -2433,6 +2433,8 @@ before_semi: } if (ngx_http_token_char(ch)) { state = sw_trailer_name; + r->lowcase_index = 1; + r->lowcase_header[0] = (ch | 0x20); break; } goto invalid; @@ -2445,9 +2447,28 @@ before_semi: case sw_trailer_name: if (ngx_http_token_char(ch)) { + if (r->lowcase_index < NGX_HTTP_LC_HEADER_LEN) { + /* ASCII uppercase letters become the lowercase ones. + * '-' is unchanged. */ + r->lowcase_header[r->lowcase_index++] = (ch | 0x20); + } break; } if (ch == ':') { + switch (r->lowcase_index) { +#define X(v) \ + case sizeof(v "") - 1: \ + if (memcmp(r->lowcase_header, v, r->lowcase_index) == 0) { \ + goto invalid; \ + } \ + break + X("transfer-encoding"); + X("content-length"); + X("upgrade"); +#undef X + default: + break; + } state = sw_trailer_value; break; }