From: Stefan Eissing Date: Tue, 22 Apr 2025 10:53:22 +0000 (+0200) Subject: http: fix HTTP/2 handling of TE request header using "trailers" X-Git-Tag: curl-8_14_0~246 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1d66a769d7b2c82e9f6942ab1fa56cd2c5385067;p=thirdparty%2Fcurl.git http: fix HTTP/2 handling of TE request header using "trailers" A "TE" request header is allowed in HTTP/2 when it only carries the "trailers" value. RFC 9113 ch. 8.2.2. Check client supplied TE values for the "trailers" token and only pass that one in a HTTP/2 request. Add test_01_17 to verify. Fixes #17122 Reported-by: epicmkirzinger on github Closes #17128 --- diff --git a/lib/http.c b/lib/http.c index 51515d1fe6..4013678519 100644 --- a/lib/http.c +++ b/lib/http.c @@ -4445,7 +4445,6 @@ struct name_const { /* keep them sorted by length! */ static struct name_const H2_NON_FIELD[] = { - { STRCONST("TE") }, { STRCONST("Host") }, { STRCONST("Upgrade") }, { STRCONST("Connection") }, @@ -4454,15 +4453,44 @@ static struct name_const H2_NON_FIELD[] = { { STRCONST("Transfer-Encoding") }, }; -static bool h2_non_field(const char *name, size_t namelen) +static bool h2_permissible_field(struct dynhds_entry *e) { size_t i; for(i = 0; i < CURL_ARRAYSIZE(H2_NON_FIELD); ++i) { - if(namelen < H2_NON_FIELD[i].namelen) + if(e->namelen < H2_NON_FIELD[i].namelen) + return TRUE; + if(e->namelen == H2_NON_FIELD[i].namelen && + strcasecompare(H2_NON_FIELD[i].name, e->name)) + return FALSE; + } + return TRUE; +} + +static bool http_TE_has_token(const char *fvalue, const char *token) +{ + while(*fvalue) { + struct Curl_str name; + + /* skip to first token */ + while(ISBLANK(*fvalue) || *fvalue == ',') + fvalue++; + if(Curl_str_cspn(&fvalue, &name, " \t\r;,")) return FALSE; - if(namelen == H2_NON_FIELD[i].namelen && - strcasecompare(H2_NON_FIELD[i].name, name)) + if(Curl_str_casecompare(&name, token)) return TRUE; + + /* skip any remainder after token, e.g. parameters with quoted strings */ + while(*fvalue && *fvalue != ',') { + if(*fvalue == '"') { + struct Curl_str qw; + /* if we do not cleanly find a quoted word here, the header value + * does not follow HTTP syntax and we reject */ + if(Curl_str_quotedword(&fvalue, &qw, CURL_MAX_HTTP_HEADER)) + return FALSE; + } + else + fvalue++; + } } return FALSE; } @@ -4521,7 +4549,14 @@ CURLcode Curl_http_req_to_h2(struct dynhds *h2_headers, } for(i = 0; !result && i < Curl_dynhds_count(&req->headers); ++i) { e = Curl_dynhds_getn(&req->headers, i); - if(!h2_non_field(e->name, e->namelen)) { + /* "TE" is special in that it is only permissible when it + * has only value "trailers". RFC 9113 ch. 8.2.2 */ + if(e->namelen == 2 && strcasecompare("TE", e->name)) { + if(http_TE_has_token(e->value, "trailers")) + result = Curl_dynhds_add(h2_headers, e->name, e->namelen, + "trailers", sizeof("trailers") - 1); + } + else if(h2_permissible_field(e)) { result = Curl_dynhds_add(h2_headers, e->name, e->namelen, e->value, e->valuelen); } diff --git a/tests/http/test_01_basic.py b/tests/http/test_01_basic.py index bb34933c0f..f820d6a7d2 100644 --- a/tests/http/test_01_basic.py +++ b/tests/http/test_01_basic.py @@ -258,3 +258,24 @@ class TestBasic: r.check_exit_code(0) else: r.check_exit_code(43) + + # http: special handling of TE request header + @pytest.mark.parametrize("te_in, te_out", [ + ['trailers', 'trailers'], + ['chunked', None], + ['gzip, trailers', 'trailers'], + ['gzip ;q=0.2;x="y,x", trailers', 'trailers'], + ['gzip ;x="trailers", chunks', None], + ]) + def test_01_17_TE(self, env: Env, httpd, te_in, te_out): + proto = 'h2' + curl = CurlClient(env=env) + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo' + r = curl.http_download(urls=[url], alpn_proto=proto, with_stats=True, + with_headers=True, + extra_args=['-H', f'TE: {te_in}']) + r.check_response(200) + if te_out is not None: + assert r.responses[0]['header']['request-te'] == te_out, f'{r.responses[0]}' + else: + assert 'request-te' not in r.responses[0]['header'], f'{r.responses[0]}' diff --git a/tests/http/testenv/mod_curltest/mod_curltest.c b/tests/http/testenv/mod_curltest/mod_curltest.c index 28b61f3468..7e6aff87b3 100644 --- a/tests/http/testenv/mod_curltest/mod_curltest.c +++ b/tests/http/testenv/mod_curltest/mod_curltest.c @@ -254,6 +254,10 @@ static int curltest_echo_handler(request_rec *r) ct = apr_table_get(r->headers_in, "content-type"); ap_set_content_type(r, ct ? ct : "application/octet-stream"); + if(apr_table_get(r->headers_in, "TE")) + apr_table_setn(r->headers_out, "Request-TE", + apr_table_get(r->headers_in, "TE")); + bb = apr_brigade_create(r->pool, c->bucket_alloc); /* copy any request body into the response */ rv = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK);