Defense-in-depth hardening for HTTP/1.1 chunked parser and HTTP/3 QPACK index computation
Summary
Source code review of nginx 1.31.0 (mainline) identified two areas for defense-in-depth hardening in protocol parsers. None are exploitable vulnerabilities under current system constraints.
Finding 1: Chunked transfer encoding lacks input length and character validation
Module: src/http/ngx_http_parse.c
Observation: sw_chunk_extension and sw_last_chunk_extension states accept unlimited extension data. sw_trailer_header accepts any byte except CR/LF (NUL, control characters pass through). No per-line length bound. There is no smuggling impact — trailer bytes in the request-body path are not stored or forwarded into a request buffer. The extension unbounded-length concern is mitigated by existing client-body timeouts.
Patch: Added NGX_HTTP_CHUNKED_MAX_EXT_LENGTH (4096 bytes) with a shared counter in ngx_http_chunked_s. Chunk/last-chunk extensions and trailer header lines reject control characters (< 0x20) and enforce the 4096-byte limit.
Finding 2: QPACK dynamic table index computation can underflow
Module: src/http/v3/ngx_http_v3_parse.c:752,860
Observation: ngx_http_v3_parse_field_ri() and ngx_http_v3_parse_field_lri() compute st->base - st->index - 1 using ngx_uint_t (64-bit unsigned). When st->index >= st->base, the result wraps to ~UINT64_MAX. The existing bounds check in ngx_http_v3_lookup() (if (index < dt->base || index - dt->base >= dt->nelts)) already catches the wrapped value for any reachable table state. Reaching a bypass would require dt->base near UINT64_MAX/2, which is not achievable.
Patch: Added explicit bounds check before subtraction in both functions, returning NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED (0x200) — defense-in-depth to fail at the parser level rather than relying on the downstream bounds check.
HTTP/2 frame_size direction — NOT a vulnerability
The original report claimed nginx fails to enforce SETTINGS_MAX_FRAME_SIZE on receive. This was incorrect: h2c->frame_size stores the peer's advertised limit — what nginx must obey when sending. nginx advertises MAX_FRAME_SIZE = 16777215 (~16MB) outbound, so a client sending frames up to 16MB is within what nginx promised to accept. No fix included for this path.
Patch
Two files, 28 insertions. Built and tested in Docker (debian:bookworm-slim, nginx 1.31.0 mainline).
--- a/src/http/ngx_http.h
+++ b/src/http/ngx_http.h
@@ -61,10 +61,14 @@
+#define NGX_HTTP_CHUNKED_MAX_EXT_LENGTH 4096
+
struct ngx_http_chunked_s {
ngx_uint_t state;
off_t size;
off_t length;
+ off_t extension_length; /* shared: chunk ext, last-chunk ext,
+ trailer line; reset by sw_chunk_start
+ and sw_trailer_header_almost_done */
};
--- a/src/http/ngx_http_parse.c
+++ b/src/http/ngx_http_parse.c
@@ -2237,6 +2237,7 @@
case sw_chunk_start:
+ ctx->extension_length = 0;
+
if (ch >= '0' && ch <= '9') {
state = sw_chunk_size;
ctx->size = ch - '0';
@@ -2305,6 +2306,9 @@
case sw_chunk_extension:
+ if (++ctx->extension_length > NGX_HTTP_CHUNKED_MAX_EXT_LENGTH) {
+ goto invalid;
+ }
switch (ch) {
case CR:
state = sw_chunk_extension_almost_done;
@@ -2346,6 +2350,9 @@
case sw_last_chunk_extension:
+ if (++ctx->extension_length > NGX_HTTP_CHUNKED_MAX_EXT_LENGTH) {
+ goto invalid;
+ }
switch (ch) {
case CR:
state = sw_last_chunk_extension_almost_done;
@@ -2387,6 +2394,12 @@
case sw_trailer_header:
+ if (ch < 0x20) {
+ goto invalid;
+ }
+ if (++ctx->extension_length > NGX_HTTP_CHUNKED_MAX_EXT_LENGTH) {
+ goto invalid;
+ }
switch (ch) {
case CR:
state = sw_trailer_header_almost_done;
@@ -2400,6 +2413,7 @@
case sw_trailer_header_almost_done:
if (ch == LF) {
state = sw_trailer;
+ ctx->extension_length = 0;
break;
}
goto invalid;
--- a/src/http/v3/ngx_http_v3_parse.c
+++ b/src/http/v3/ngx_http_v3_parse.c
@@ -749,6 +749,11 @@
if (st->dynamic) {
+ if (st->index >= st->base) {
+ ngx_log_error(NGX_LOG_INFO, c->log, 0,
+ "http3 parse field ri index exceeds base");
+ return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+ }
st->index = st->base - st->index - 1;
}
@@ -862,6 +867,11 @@
if (st->dynamic) {
+ if (st->index >= st->base) {
+ ngx_log_error(NGX_LOG_INFO, c->log, 0,
+ "http3 parse field lri index exceeds base");
+ return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+ }
st->index = st->base - st->index - 1;
}Source: nginx/nginx