]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
http: fix HTTP/2 handling of TE request header using "trailers"
authorStefan Eissing <stefan@eissing.org>
Tue, 22 Apr 2025 10:53:22 +0000 (12:53 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 22 Apr 2025 13:55:36 +0000 (15:55 +0200)
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

lib/http.c
tests/http/test_01_basic.py
tests/http/testenv/mod_curltest/mod_curltest.c

index 51515d1fe62842457c110006dee29342acc19b2f..4013678519d5f93557336ed2a1c3b052c10c03cd 100644 (file)
@@ -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);
     }
index bb34933c0f086b4418bf59179929d61b0b8d100d..f820d6a7d20f0a16780c2b981cb45b8e9d79615c 100644 (file)
@@ -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]}'
index 28b61f3468cc27331d06d1e73d83158d3eaa4653..7e6aff87b3240503fe1c1ecc3cdf62537172dfe8 100644 (file)
@@ -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);