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
This commit is contained in:
Stefan Eissing 2025-04-22 12:53:22 +02:00 committed by Daniel Stenberg
parent ebe6612505
commit 1d66a769d7
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
3 changed files with 67 additions and 7 deletions

View 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)
return FALSE;
if(namelen == H2_NON_FIELD[i].namelen &&
strcasecompare(H2_NON_FIELD[i].name, name))
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(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);
}

View 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]}'

View 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);