]> git.kaiwu.me - haproxy.git/commitdiff
BUG/MEDIUM: h3: split absolute URIs like the HTTP analysers do on output
authorOlivier Houchard <ohouchard@haproxy.com>
Wed, 7 Oct 2026 09:20:11 +0000 (11:20 +0200)
committerOlivier Houchard <cognet@ci0.org>
Mon, 19 Oct 2026 18:56:23 +0000 (20:56 +0200)
h3_req_headers_send() derived :authority and :path from absolute-form
URIs with the same flaw as the H2 mux, fixed by the previous commit:
the authority ended at the first '/' instead of the first '/', '?' or
'#' (RFC3986#3.2), the userinfo was not stripped, and an empty path
was sent as an empty :path. A request evaluated by haproxy for one
host/path could be delivered to an H3 server with another one,
bypassing host and path based rules.

Now the authority is extracted like http_parse_authority() does, the
userinfo is stripped, and :path always starts with a '/' (or is '*'
for OPTIONS). As H3 backends are still experimental, the impact is
limited.

This was introduced by commit 235e818fa ("MINOR: h3: complete HTTP/3
request scheme encoding") in 3.3. Must be backported as far as 3.3.

src/h3.c

index 78655e314a91d2b0a6858ee72d79887425757da6..a5d53f40cd06c39117a5bdfb7d8920a80b6b6c20 100644 (file)
--- a/src/h3.c
+++ b/src/h3.c
@@ -2425,7 +2425,7 @@ static int h3_req_headers_send(struct qcs *qcs, struct htx *htx, char *fin)
        enum htx_blk_type type;
        struct htx_blk *blk;
        struct htx_sl *sl;
-       struct ist meth, uri, scheme = IST_NULL, auth = IST_NULL;
+       struct ist meth, uri, scheme = IST_NULL, auth = IST_NULL, at;
        int frame_length_size;  /* size in bytes of frame length varint field */
        int smallbuf = 1;
        int ret = 0;
@@ -2511,15 +2511,35 @@ static int h3_req_headers_send(struct qcs *qcs, struct htx *htx, char *fin)
                        scheme = ist2(uri.ptr, len);
                        uri = istadv(uri, len + 3);
 
-                       /* find the auth part of the URI */
+                       /* find the auth part of the URI. As per RFC3986#3.2,
+                        * it ends at the first '/', '?' or '#'.
+                        */
                        auth = ist2(uri.ptr, 0);
-                       while (auth.len < uri.len && auth.ptr[auth.len] != '/')
+                       while (auth.len < uri.len && auth.ptr[auth.len] != '/' &&
+                              auth.ptr[auth.len] != '?' && auth.ptr[auth.len] != '#')
                                auth.len++;
 
                        uri = istadv(uri, auth.len);
+
+                       for (at = ist2(istend(auth), 0); at.ptr > auth.ptr; at.ptr--) {
+                               if (at.ptr[-1] == '@') {
+                                       auth = ist2(at.ptr, istend(auth) - at.ptr);
+                                       break;
+                               }
+                       }
                }
        }
 
+       if (!uri.len)
+               uri = ((sl->info.req.meth == HTTP_METH_OPTIONS) ? ist("*") : ist("/"));
+       else if (*uri.ptr != '/' && *uri.ptr != '*') {
+               struct buffer *path = get_trash_chunk();
+
+               if (!chunk_memcat(path, "/", 1) || !chunk_memcat(path, uri.ptr, uri.len))
+                       goto err;
+               uri = ist2(b_orig(path), b_data(path));
+       }
+
        if (!istlen(scheme)) {
                if ((sl->flags & (HTX_SL_F_HAS_SCHM|HTX_SL_F_SCHM_HTTP)) == (HTX_SL_F_HAS_SCHM|HTX_SL_F_SCHM_HTTP))
                        scheme = ist("http");