Version in base suite: 3.0.11-1+deb13u2 Base version: haproxy_3.0.11-1+deb13u2 Target version: haproxy_3.0.11-1+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/h/haproxy/haproxy_3.0.11-1+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/h/haproxy/haproxy_3.0.11-1+deb13u3.dsc changelog | 22 + patches/0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch | 63 +++ patches/0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch | 57 +++ patches/0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch | 113 ++++++ patches/0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch | 75 ++++ patches/0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch | 85 ++++ patches/0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch | 152 ++++++++ patches/0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch | 73 ++++ patches/0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch | 175 ++++++++++ patches/0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch | 92 +++++ patches/0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch | 86 ++++ patches/0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch | 49 ++ patches/0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch | 46 ++ patches/0001-MINOR-http-add-a-function-to-validate-characters-of-.patch | 97 +++++ patches/series | 17 15 files changed, 1202 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp09tdlf6p/haproxy_3.0.11-1+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp09tdlf6p/haproxy_3.0.11-1+deb13u3.dsc: no acceptable signature found diff -Nru haproxy-3.0.11/debian/changelog haproxy-3.0.11/debian/changelog --- haproxy-3.0.11/debian/changelog 2026-02-11 21:52:43.000000000 +0000 +++ haproxy-3.0.11/debian/changelog 2026-05-20 19:21:22.000000000 +0000 @@ -1,3 +1,25 @@ +haproxy (3.0.11-1+deb13u3) trixie-security; urgency=high + + [ Salvatore Bonaccorso ] + * BUG/MAJOR: h3: check body size with content-length on empty FIN + (CVE-2026-33555) + + [ Vincent Bernat ] + * BUG/MAJOR: mux-h2: detect incomplete transfers on HEADERS frames as + well + * BUG/MAJOR: http-htx: Store new host in a chunk for scheme-based + normalization + * BUG/MAJOR: mux-h1: Deal with true 64-bits integer to emit chunks size + * BUG/MAJOR: mux-h2: preset MSGF_BODY_CL on H2_SF_DATA_CLEN in + h2c_dec_hdrs() + * BUG/MAJOR: slz: always make sure to limit fixed output to less than + worst case literals + * BUG/MAJOR: http: forbid comma character in authority value + * BUG/MEDIUM: h1: Skip all h2c values from Upgrade headers during + parsing + + -- Vincent Bernat Wed, 20 May 2026 21:21:22 +0200 + haproxy (3.0.11-1+deb13u2) trixie-security; urgency=high * CVE-2026-26081: fix integer overflow in QUIC code. diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,63 @@ +From: Amaury Denoyelle +Date: Wed, 18 Mar 2026 09:24:32 +0100 +Subject: BUG/MAJOR: h3: check body size with content-length on empty FIN +Origin: https://git.haproxy.org/?p=haproxy-3.0.git;a=commit;h=425b969d6ea4114f4ae260f57802c65ccafc319c +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-33555 + +In QUIC, a STREAM frame may be received with no data but with FIN bit +set. This situation is tedious to handle and haproxy parsing code has +changed several times to deal with this situation. Now, H3 and H09 +layers parsing code are skipped in favor of the shared function +qcs_http_handle_standalone_fin() used to handle the HTX EOM emission. + +However, this shortcut bypasses an important HTTP/3 validation check on +the received body size vs the announced content-length header. Under +some conditions, this could cause a desynchronization with the backend +server which could be exploited for request smuggling. + +Fix HTTP/3 parsing code by adding a call to h3_check_body_size() prior +to qcs_http_handle_standalone_fin() if content-length header has been +found. If the body size is incorrect, the stream is immediately resetted +with H3_MESSAGE_ERROR code and the error is forwarded to the stream +layer. + +Thanks to Martino Spagnuolo for his detailed report on this issue and +for having contacting us about it via the security mailing list. + +This must be backported up to 2.6. + +(cherry picked from commit 05a295441c621089ffa4318daf0dbca2dd756a84) +Signed-off-by: Amaury Denoyelle +(cherry picked from commit 18e450ab412fb9397da36f226db8eed31ab590b6) +Signed-off-by: Amaury Denoyelle +(cherry picked from commit 7ab4ae974c434e62896b3c68b7b485b9dceb7a25) +[ad: remove usage of qcs_http_handle_standalone_fin() introduced in 3.2] +Signed-off-by: Amaury Denoyelle +(cherry picked from commit ae54ad97c84cd6173f134c1f052a5375cf704f5c) +Signed-off-by: Amaury Denoyelle +--- + src/h3.c | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/src/h3.c b/src/h3.c +index c37a49256112..821f422972cc 100644 +--- a/src/h3.c ++++ b/src/h3.c +@@ -1418,6 +1418,14 @@ static ssize_t h3_rcv_buf(struct qcs *qcs, struct buffer *b, int fin) + int eom; + + TRACE_PROTO("received FIN without data", H3_EV_RX_FRAME, qcs->qcc->conn, qcs); ++ ++ /* FIN received, ensure body length is conform to any content-length header. */ ++ if ((h3s->flags & H3_SF_HAVE_CLEN) && h3_check_body_size(qcs, 1)) { ++ qcc_abort_stream_read(qcs); ++ qcc_reset_stream(qcs, h3s->err); ++ goto done; ++ } ++ + if (!(appbuf = qcc_get_stream_rxbuf(qcs))) { + TRACE_ERROR("data buffer alloc failure", H3_EV_RX_FRAME, qcs->qcc->conn, qcs); + qcc_set_error(qcs->qcc, H3_ERR_INTERNAL_ERROR, 1); +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,57 @@ +From d0798ebc1522ddc0830d9ace23eaa9676039440f Mon Sep 17 00:00:00 2001 +From: Christopher Faulet +Date: Tue, 28 Apr 2026 09:37:34 +0200 +Subject: [PATCH] BUG/MAJOR: http: forbid comma character in authority value + +Strictly speaking, the comma character in authority is allowed by RFC3986. +However, it is pretty ambiguous for Host header value because comma is also +the value separator for headers supporting multiple value. It is also very +unlikely to have comma in host header value or authority. So instead of +dealing with this case with all the risks of bugs that this entails, we've +decided to forbid the comma in authority and host header value during the +parsing. Concretely, only http_authority_has_forbidden_char() was updated. + +The internal API was not updated to prevent comma to be inserted when the +host header value is updated for instance. But this should be so uncommon +that it is not really a concern. + +This patch should be backported as far as 2.8. For previous verions, +http_authority_has_forbidden_char() function does not exist. + +(cherry picked from commit b743b566b4215ed49b95d2fc22054ffe66202a0f) +Signed-off-by: Willy Tarreau +(cherry picked from commit ec5d64fff23b1a10364b353d00f4995661b3a8be) +Signed-off-by: Willy Tarreau +(cherry picked from commit 9354d90142d3673c1b4d27b9d81124102c3f8aa4) +Signed-off-by: Willy Tarreau +--- + include/haproxy/http.h | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/include/haproxy/http.h b/include/haproxy/http.h +index af1bb3cf24c8..045abe1e59d2 100644 +--- a/include/haproxy/http.h ++++ b/include/haproxy/http.h +@@ -238,6 +238,11 @@ static inline int http_path_has_forbidden_char(const struct ist ist, const char + * fall back to the slow path and decide. Brackets are used for IP-literal and + * deserve special case, that is better handled in the slow path. The function + * returns 0 if no forbidden char is presnet, non-zero otherwise. ++ * ++ * There is a special case for the comma (','). While it is allowed, we reject ++ * it because the authority is higly linked with the host header. The comma is ++ * also the header value separator. So it is highly ambiguous to use it for the ++ * authority/host value. + */ + static inline int http_authority_has_forbidden_char(const struct ist ist) + { +@@ -257,6 +262,7 @@ static inline int http_authority_has_forbidden_char(const struct ist ist) + c = p[ofs]; + + if (unlikely(c < 0x21 || c > 0x7e || ++ c == ',' || /* Special case: forbidden because it is ambiguous for the host header value */ + c == '#' || c == '/' || c == '?' || c == '@' || + c == '[' || c == '\\' || c == ']')) { + /* all of them must be rejected, except '[' which may +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,113 @@ +From 904b439314f670b9c8b096894b2cbc6e4b73c54f Mon Sep 17 00:00:00 2001 +From: Christopher Faulet +Date: Tue, 28 Apr 2026 18:15:15 +0200 +Subject: [PATCH] BUG/MAJOR: mux-h1: Deal with true 64-bits integer to emit + chunks size + +Functions emitting chunks size are using size_t integer to do so. Depending +on the code path, these functions can be called using an unsigned long long +integer (h1m->curr_len for instance). On 64-bits architectures, there is no +issue. But on the 32-bits architecture, it is a problem. size_t are 32-bits +integer so the 64-bits parameter will be casted to a 32-bits integer. For +chunk size exceeding 4GB, the wrong size will be emitted. + +To fix the issue, these functions are now using true 64-bits +integer. h1s_consume_kop() was also modified accordingly. + +In addition, when a size_t is compared to a 64-bits integer, an explicit +cast is used to be sure the right type is used. + +This patch must be backported as far as 3.0. It must be backported after +1ef74fc7c ("BUG/MEDIUM: mux_h1: fix stack buffer overflow in +h1_append_chunk_size()"). + +(cherry picked from commit f9c2d476777be2a1cd15658071fb237822bb1699) +Signed-off-by: Christopher Faulet +(cherry picked from commit 7fb16f949fb32a58ecad1754fcbe85520756df05) +[cf: h1s_consume_kop() does not exist on 3.2] +Signed-off-by: Christopher Faulet +(cherry picked from commit 965c8430c3c735008240a6264e1b02b8cf65a004) +Signed-off-by: Christopher Faulet +--- + src/mux_h1.c | 18 +++++++++--------- + 1 file changed, 9 insertions(+), 9 deletions(-) + +diff --git a/src/mux_h1.c b/src/mux_h1.c +index 3bfa2328ecf5..1cfbb36971dd 100644 +--- a/src/mux_h1.c ++++ b/src/mux_h1.c +@@ -1680,7 +1680,7 @@ static void h1_capture_bad_message(struct h1c *h1c, struct h1s *h1s, + * responsibility to pass the right value. if is set to 0 (or less that + * the smallest size to represent the chunk size), it is ignored. + */ +-static void h1_prepend_chunk_size(struct buffer *buf, size_t chksz, size_t length) ++static void h1_prepend_chunk_size(struct buffer *buf, uint64_t chksz, size_t length) + { + char *beg, *end; + +@@ -1705,7 +1705,7 @@ static void h1_prepend_chunk_size(struct buffer *buf, size_t chksz, size_t lengt + /* Emit the chunksize followed by a CRLF after the data of the buffer + * . Returns 0 on error. + */ +-static int h1_append_chunk_size(struct buffer *buf, size_t chksz) ++static int h1_append_chunk_size(struct buffer *buf, uint64_t chksz) + { + char tmp[18]; + char *beg, *end; +@@ -3016,13 +3016,13 @@ static size_t h1_make_data(struct h1s *h1s, struct h1m *h1m, struct buffer *buf, + h1m->state = H1_MSG_DATA; + } + +- if (vlen > h1m->curr_len) { ++ if ((uint64_t)vlen > h1m->curr_len) { + vlen = h1m->curr_len; + last_data = 0; + } + + chklen = 0; +- if (h1m->curr_len == vlen) ++ if (h1m->curr_len == (uint64_t)vlen) + chklen += 2; + if (last_data) + chklen += 5; +@@ -4718,7 +4718,7 @@ static size_t h1_nego_ff(struct stconn *sc, struct buffer *input, size_t count, + } + + if (h1m->flags & H1_MF_CLEN) { +- if ((flags & NEGO_FF_FL_EXACT_SIZE) && count > h1m->curr_len) { ++ if ((flags & NEGO_FF_FL_EXACT_SIZE) && (uint64_t)count > h1m->curr_len) { + TRACE_ERROR("more payload than announced", H1_EV_STRM_SEND|H1_EV_STRM_ERR, h1c->conn, h1s); + h1s->sd->iobuf.flags |= IOBUF_FL_NO_FF; + goto out; +@@ -4727,8 +4727,8 @@ static size_t h1_nego_ff(struct stconn *sc, struct buffer *input, size_t count, + else if (h1m->flags & H1_MF_CHNK) { + if (h1m->curr_len) { + BUG_ON(h1m->state != H1_MSG_DATA); +- if (count > h1m->curr_len) { +- if ((flags & NEGO_FF_FL_EXACT_SIZE) && count > h1m->curr_len) { ++ if ((uint64_t)count > h1m->curr_len) { ++ if ((flags & NEGO_FF_FL_EXACT_SIZE) && (uint64_t)count > h1m->curr_len) { + TRACE_ERROR("chunk bigger than announced", H1_EV_STRM_SEND|H1_EV_STRM_ERR, h1c->conn, h1s); + h1s->sd->iobuf.flags |= IOBUF_FL_NO_FF; + goto out; +@@ -4955,7 +4955,7 @@ static int h1_fastfwd(struct stconn *sc, unsigned int count, unsigned int flags) + retry: + ret = 0; + +- if (h1m->state == H1_MSG_DATA && (h1m->flags & (H1_MF_CHNK|H1_MF_CLEN)) && count > h1m->curr_len) { ++ if (h1m->state == H1_MSG_DATA && (h1m->flags & (H1_MF_CHNK|H1_MF_CLEN)) && (uint64_t)count > h1m->curr_len) { + nego_flags |= NEGO_FF_FL_EXACT_SIZE; + count = h1m->curr_len; + } +@@ -5034,7 +5034,7 @@ static int h1_fastfwd(struct stconn *sc, unsigned int count, unsigned int flags) + + out: + if (h1m->state == H1_MSG_DATA && (h1m->flags & (H1_MF_CHNK|H1_MF_CLEN))) { +- if (total > h1m->curr_len) { ++ if ((uint64_t)total > h1m->curr_len) { + h1s->flags |= H1S_F_PARSING_ERROR; + se_fl_set(h1s->sd, SE_FL_ERROR); + TRACE_ERROR("too much payload, more than announced", +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,75 @@ +From fba3f0a1e5b3682fc51d291aa5385e1b75d447a7 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Wed, 22 Apr 2026 14:54:20 +0200 +Subject: [PATCH] BUG/MAJOR: mux-h2: detect incomplete transfers on HEADERS + frames as well + +Checks are already made on H2 to detect inconsistencies between +advertised content-length and transferred data (excess of data or +premature END_STREAM flag on DATA frame). However, as found by +Martino Spagnuolo (r3verii), a subtle case remains: if the END_STREAM +appears on the HEADERS frame (i.e. a regular request for example), +then the check is not made. In this case it is possible to advertise +more contents than will really be transferred. If the other side uses +HTTP/1.1, and the server responds before the end of the transfer, +this means that the number of advertised bytes that will never be +transferred and that the server will drain will be taken from the +next request, effectively hiding a part of the header. + +In practice this can be used to force subsequent requests to fail, or +when running with "http-reuse never" or when running with a totally +idle server, to perform a request smuggling by constructing specially +crafted request pairs where the first one is used to trigger an early +response and hide parts of or all headers of the second one, to +instead use a second embedded one that was not subject to analysis. + +The risk remains moderate given the low prevalence of "http-reuse never" +in production environments, and of idle servers. + +The fix consists in detecting if advertised content-length remains when +processing an END_STREAM flag on a HEADERS frame. It also does it for +trailers, which turn out to be another way to abuse the bug. However it +takes great care not to break bodyless responses (204, 304 and responses +to HEAD requests) that may present a content-length that doesn't reflect +the presence of a body in the response. + +A temporary alternative to the fix is to disable HTTP/2 by specifying +"alpn http/1.1" on "bind" lines, and adding "option disable-h2-upgrade" +in HTTP frontends. + +This must be backported to all stable versions. + +(cherry picked from commit d12edebe4af20b9cc14a21318d54925edba23b59) +Signed-off-by: Willy Tarreau +(cherry picked from commit fbcd6382ff39f291125a38031fd95866e53e23f8) +Signed-off-by: Willy Tarreau +(cherry picked from commit 97efe293bdfe3b8a9ba8beffbf61f22832a3773c) +[wt: drop the description in h2c_report_glitch() for 3.0] +Signed-off-by: Willy Tarreau +--- + src/mux_h2.c | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/src/mux_h2.c b/src/mux_h2.c +index 819397a2e91e..bd12e55a8f93 100644 +--- a/src/mux_h2.c ++++ b/src/mux_h2.c +@@ -5525,6 +5525,15 @@ static int h2c_dec_hdrs(struct h2c *h2c, struct buffer *rxbuf, uint32_t *flags, + TRACE_STATE("invalid interim response with ES flag", H2_EV_RX_FRAME|H2_EV_RX_HDR|H2_EV_H2C_ERR|H2_EV_PROTO_ERR, h2c->conn); + goto fail; + } ++ /* Note that bodyless only applies to responses, even when ++ * reported on the request (e.g. HEAD). ++ */ ++ if ((msgf & H2_MSGF_BODY_CL) && *body_len > 0 && ++ (!(h2c->flags & H2_CF_IS_BACK) || !(*flags & H2_SF_BODYLESS_RESP))) { ++ h2c_report_glitch(h2c, 1); ++ TRACE_STATE("ES on HEADERS before end of content-length", H2_EV_RX_FRAME|H2_EV_RX_HDR|H2_EV_H2C_ERR|H2_EV_PROTO_ERR, h2c->conn); ++ goto fail; ++ } + /* no more data are expected for this message */ + htx->flags |= HTX_FL_EOM; + *flags |= H2_SF_ES_RCVD; +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,85 @@ +From 227d1055b0e27df73f383185fb9b35752140a4da Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Mon, 4 May 2026 13:51:36 +0200 +Subject: [PATCH] BUG/MAJOR: mux-h2: preset MSGF_BODY_CL on H2_SF_DATA_CLEN in + h2c_dec_hdrs() + +Commit d12edebe4a ("BUG/MAJOR: mux-h2: detect incomplete transfers on +HEADERS frames as well") tried to enforce strict matching between +advertised content-length and transferred data when dealing with ES on +a headers frame. It purposely arranged the code so that it would cover +both headers and trailers. The problem is, in h2c_dec_hdrs() we preset +message flags (msgf) based on the current state and knowledge related +to the stream being processed, then we pass these flags to the headers +parser and use their final state to perform some extra checks. + +MSGF_BODY_CL was set by the parsers themselves when processing a +content-length header, but when parsing a trailers frame, it will not +be set, and due to this the matching between the remaining expected +content-length and the transferred data is not verified, so the fix +above doesn't work for trailers. + +This patch sets MSGF_BODY_CL to the same value as H2_SF_DATA_CLEN so +that during headers it remains zero, but it matches what was learned +during headers when processing trailers. It is sufficient to re-enable +the check that was attempted in the commit above. + +The impact remains the same as the one indicated in the commit above: in +practice this can be used to force subsequent requests to fail, or when +running with "http-reuse never" or when running with a totally idle server, +to perform a request smuggling by constructing specially crafted request +pairs where the first one is used to trigger an early response and hide +parts of or all headers of the second one, to instead use a second +embedded one that was not subject to analysis. As such, the risk remains +moderate given the low prevalence of "http-reuse never" in production +environments, and of idle servers. Again, a temporary alternative to the +fix is to disable HTTP/2 by specifying "alpn http/1.1" on "bind" lines, +and adding "option disable-h2-upgrade" in HTTP frontends. + +Many thanks to Pratham Gupta / alchemy1729 for spotting and analyzing +this problem, and for providing a lightweight reproducer to illustrate +the problem! + +This fix must be backported to all versions where the fix above was +backported (i.e. all). Note that it depends on this previous commit +otherwise trailers will always break: + + BUG/MEDIUM: mux-h2: fix the body_len to check when parsing request trailers + +As a side note, it's worth noting that these temporary message flags have +reached a level of pain and fragility that really warrants a complete +rework. Ideally we should have a pair of such flags in the h2s (one per +direction) and the callers of the parsers should point to them so that +they are always up to date. And by having generic HTTP flags instead of +H2, we could better unify the h1/h2/h3/fcgi processors (and maybe avoid +some HTX conversion). One flag could even indicate that trailers are +being parsed (since they're last) so as to ease this detection down the +chain. + +(cherry picked from commit 7e09e2a916af400f8cde8dabf649adf7fc1f6600) +Signed-off-by: Willy Tarreau +(cherry picked from commit b532e29ae647e119cb10460e8f66e2ce30a0ddf7) +Signed-off-by: Willy Tarreau +(cherry picked from commit 8c84aaec02e103e3ea3243975c4bca6a7f1b406a) +Signed-off-by: Willy Tarreau +--- + src/mux_h2.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/mux_h2.c b/src/mux_h2.c +index 2253286f2982..10cce46ccdcc 100644 +--- a/src/mux_h2.c ++++ b/src/mux_h2.c +@@ -5473,6 +5473,9 @@ static int h2c_dec_hdrs(struct h2c *h2c, struct buffer *rxbuf, uint32_t *flags, + * to convert 200 response to 101 htx response */ + msgf |= (*flags & H2_SF_EXT_CONNECT_SENT) ? H2_MSGF_EXT_CONNECT: 0; + ++ /* when dealing with trailers, we need to check the content-length */ ++ msgf |= (*flags & H2_SF_DATA_CLEN) ? H2_MSGF_BODY_CL : 0; ++ + if (*flags & H2_SF_HEADERS_RCVD) + goto trailers; + +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,152 @@ +From 1d5c2e67e318f526998244cfa893b11b115a37b0 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Wed, 8 Apr 2026 17:53:24 +0200 +Subject: [PATCH] BUG/MAJOR: slz: always make sure to limit fixed output to + less than worst case literals + +Literals are sent in two ways: + - in EOB state, unencoded and prefixed with their length + - in FIXED state, huffman-encoded + +And references are only sent in FIXED state. + +The API promises that the amount of data will not grow by more than +5 bytes every 65535 input bytes (the comment was adjusted to remind +this last point). This is guaranteed by the literal encoding in EOB +state (BT, LEN, NLEN + bytes), which is supposed to be the worst +case by design. + +However, as reported by Greg KH, this is currently not true: the test +that decides whether or not to switch to FIXED state to send references +doesn't properly account for the number of bytes needed to roll back +to the *exact* same state in EOB, which means sending EOB, BT, +alignment, LEN and NLEN in addition to the referenced bytes, versus +sending the encoding for the reference. By not taking into account the +cost of returning to the initial state (BT+LEN+NLEN), it was possible +to stay too long in the FIXED state and to consume the extra bytes that +are needed to return to the EOB state, resulting in producing much more +data in case of multiple switchovers (up to 6.25% increase was measured +in tests, or 1/16, which matches worst case estimates based on the code). + +And this check is only valid when starting from EOB (in order to restore +the same state that offers this guarantee). When already in FIXED state, +the encoded reference is always smaller than or same size as the data. +The smallest match length we support is 4 bytes, and when encoded this +is no more than 28 bits, so it is safe to stay in FIXED state as long +as needed while checking the possibility of switching back to EOB. + +This very slightly reduces the compression ratio (-0.17% on a linux +kernel source) but makes sure we respect the API promise of no more +than 5 extra bytes per 65535 of input. A side effect of the slightly +simpler check is an ~7.5% performance increase in compression speed. + +Many thanks to Greg for the detailed report allowing to reproduce +the issue. + +This is libslz upstream commit 002e838935bf298d967f670036efa95822b6c84e. + +Note: in haproxy's default configuration (tune.bufsize 16384, +tune.maxrewrite 1024), this problem cannot be triggered, because the +reserve limits input to 15360 bytes, and the overflow is maximum +960 bytes resulting in 16320 bytes total, which still fits into the +buffer. However, reducing tune.maxrewrite below 964, or tune.bufsize +above 17408 can result in overflows for specially crafted patterns. + +A workaround for larger buffers consists in always setting tune.bufsize +to at least 1/16 of tune.bufsize. + +Reported-by: Greg Kroah-Hartman +Link: https://www.mail-archive.com/haproxy@formilux.org/msg46837.html +(cherry picked from commit 3020fde525896bc863fd2b6f43ac313f13a08dfa) +Signed-off-by: Willy Tarreau +(cherry picked from commit 5ca993fabc3b8a91c35ab4f0fc8fa0b8e6763a36) +Signed-off-by: Willy Tarreau +(cherry picked from commit fc11060263761ce4aa9aa9e3ed5036f6e0d7b107) +Signed-off-by: Christopher Faulet +--- + src/slz.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++-------- + 1 file changed, 53 insertions(+), 8 deletions(-) + +diff --git a/src/slz.c b/src/slz.c +index 1560bac343fb..7a2966829730 100644 +--- a/src/slz.c ++++ b/src/slz.c +@@ -499,11 +499,11 @@ static void reset_refs(union ref *refs, long count) + } + + /* Compresses bytes from into according to RFC1951. The +- * output result may be up to 5 bytes larger than the input, to which 2 extra +- * bytes may be added to send the last chunk due to BFINAL+EOB encoding (10 +- * bits) when is not set. The caller is responsible for ensuring there +- * is enough room in the output buffer for this. The amount of output bytes is +- * returned, and no CRC is computed. ++ * output result may be up to 5 bytes larger than the input for each 65535 ++ * nput bytes, to which 2 extra bytes may be added to send the last chunk due ++ * to BFINAL+EOB encoding (10 bits) when is not set. The caller is ++ * responsible for ensuring there is enough room in the output buffer for this. ++ * The amount of output bytes is returned, and no CRC is computed. + */ + long slz_rfc1951_encode(struct slz_stream *strm, unsigned char *out, const unsigned char *in, long ilen, int more) + { +@@ -633,10 +633,55 @@ long slz_rfc1951_encode(struct slz_stream *strm, unsigned char *out, const unsig + /* direct mapping of dist->huffman code */ + dist = fh_dist_table[pos - last - 1]; + +- /* if encoding the dist+length is more expensive than sending +- * the equivalent as bytes, lets keep the literals. ++ /* If encoding the dist+length is more expensive than sending ++ * the equivalent as bytes, lets keep the literals. This is ++ * important for the compression ratio and also for standing by ++ * our promise that the output is no longer than input + 5 bytes ++ * every 65535. The explanation is that a valid stream starts in ++ * EOB and ends in EOB, which is the state where we can grow by ++ * up to 5 bytes (1 byte for BT, 2 for LEN, 2 for NLEN, then ++ * literal data). Sending compressed Huffman data (reference or ++ * fixed encoding) may only be accepted if it is guaranteed that ++ * it will not represent more bytes in the worst case. Switching ++ * from EOB to FIXED requires 3 bits of BT. Then either fixed ++ * data (8 or 9 bits) or references (15-33 bits). Switching back ++ * to EOB requires EOB (7 bits), and in order to get back to the ++ * situation where bytes can be added with no overhead, the BT, ++ * align, LEN and NLEN have to be sent as well: ++ * ++ * Before switching: ++ * [BT] [ALIGN] [LEN] [NLEN] [PLIT] ++ * bits: 3 7 16 16 8*plit ++ * ++ * In case of sending mlen bytes as literals: ++ * [BT] [ALIGN] [LEN] [NLEN] [PLIT] [MLEN] ++ * bits: 3 7 16 16 8*plit 8*mlen ++ * ++ * In case of sending mlen bytes as literals, we add: ++ * [MLEN] ++ * bits: 8*mlen ++ * ++ * In case of sending reference, we add in the worst case: ++ * [BT] [CODE] [DIST] [EOB] [BT] [ALIGN] [LEN] [NLEN] ++ * bits: 3 clen dlen 7 3 7 16 16 ++ * ++ * Thus for literals we add 8*mlen bits, and for reference we ++ * add clen+dlen+52 bits. If the reference encoding + 52 bits ++ * is shorter. Of course there are plenty of opportunities to ++ * be much shorter once we switch to FIXED, and a stricter ++ * tracking could allow to send references more often. But here ++ * we at least guarantee that if data fit as literals, they also ++ * fit using a temporary switch to FIXED. ++ * ++ * Regarding encoding size, clen is 7 for mlen 3..10, to 12 for ++ * mlen 131..257. dlen is 8 for distances 1..4, to 21 for ++ * distances 16385..32768. Thus mlen <= 130 produces clen+dlen ++ * <= 32 bits. This means that for mlen 4 or above, the encoded ++ * reference is always smaller than the data it references. This ++ * is what guarantees that once switched to FIXED we can stay ++ * in it for as long as needed. + */ +- if ((dist & 0x1f) + (code >> 16) + 8 >= 8 * mlen + bit9) ++ if (strm->state == SLZ_ST_EOB && (dist & 0x1f) + (code >> 16) + 52 > 8 * mlen) + goto send_as_lit; + + /* first, copy pending literals */ +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,73 @@ +From 715c0da9e50426ddd85ef10187cd64010bc494ac Mon Sep 17 00:00:00 2001 +From: Christopher Faulet +Date: Tue, 28 Apr 2026 11:20:59 +0200 +Subject: [PATCH] BUG/MEDIUM: h1: Enforce the authority validation during H1 + request parsing + +When a H1 request was parsed, only a light validation was performed on the +URI, mainly because there was no distinction between the different parts of +the URI. So only characters in the range [0x21, 0x7e], excluding the "#" was +allowed. + +To be consistant with the H2 and H3 parser, the authority is now validated, +using http_authority_has_forbidden_char() function. + +This patch should be backported as far as 2.8. For previous verions, +http_authority_has_forbidden_char() function does not exist. + +(cherry picked from commit a46b0eec72e01dbbf436c8bd6917d9e4097ba8c3) +Signed-off-by: Willy Tarreau +(cherry picked from commit 1ddde22216499a7d3cff4dcd29f7d72e3f9af9fb) +Signed-off-by: Willy Tarreau +(cherry picked from commit 5c1a48d0f36d9848d8bf3a4403f08b2951d55e1a) +Signed-off-by: Willy Tarreau +--- + src/h1.c | 18 ++++++++++++++++-- + 1 file changed, 16 insertions(+), 2 deletions(-) + +diff --git a/src/h1.c b/src/h1.c +index c8c56572197f..6b29380235c2 100644 +--- a/src/h1.c ++++ b/src/h1.c +@@ -1164,6 +1164,22 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + + case URI_PARSER_FORMAT_ABSURI_OR_AUTHORITY: + scheme = http_parse_scheme(&parser); ++ authority = http_parse_authority(&parser, 1); ++ if (http_authority_has_forbidden_char(authority)) { ++ if (h1m->err_pos < -1) { ++ state = H1_MSG_LAST_LF; ++ /* WT: gcc seems to see a path where sl.rq.u.ptr was used ++ * uninitialized, but it doesn't know that the function is ++ * called with initial states making this impossible. ++ */ ++ ALREADY_CHECKED(sl.rq.u.ptr); ++ ptr = sl.rq.u.ptr; /* Set ptr on the error */ ++ goto http_msg_invalid; ++ } ++ if (h1m->err_pos == -1) /* capture the error pointer */ ++ h1m->err_pos = sl.rq.u.ptr - start + skip; /* >= 0 now */ ++ } ++ + if (!isttest(scheme)) { /* scheme not found: MUST be an authority */ + struct ist *host = NULL; + +@@ -1173,7 +1189,6 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + } + if (host_idx != -1) + host = &hdr[host_idx].v; +- authority = http_parse_authority(&parser, 1); + ret = h1_validate_connect_authority(scheme, authority, host); + if (ret < 0) { + if (h1m->err_pos < -1) { +@@ -1200,7 +1215,6 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + + if (host_idx != -1) + host = hdr[host_idx].v; +- authority = http_parse_authority(&parser, 1); + /* For non-CONNECT method, the authority must match the host header value */ + if (isttest(host) && !isteqi(authority, host)) { + ret = h1_validate_mismatch_authority(scheme, authority, host); +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,175 @@ +commit 8dd49dfabaf60f95da0387d0c0b5d4060dc28a6b +Author: Christopher Faulet +Date: Mon May 18 16:09:30 2026 +0200 + + BUG/MEDIUM: h1: Skip all h2c values from Upgrade headers during parsing + + During the H1 message parsing, the Upgrade header values are checked to + detect "h2c" and "h2" tokens and skip them. To do so, we rely on + H1_MF_UPG_H2C flag, set during the parsing. And during the request + post-parsing, if this flag was set, all Upgrade headers are removed. + + This was fixed by the commit 7b89aa5b1 ("BUG/MINOR: h1: do not forward h2c + upgrade header token"). + + However, there are two issues here and the commit above must be refined. + First, the flag is reset for each new Upgrade header. So "h2c" or "h2" + tokens will be properly detected if all tokens are set on the same Upgrade + header. But if splitted on several headers, previously detected tokens will + be hidden by a next ones. + + Concretly, the following will be properly caught + + Connection: upgrade + Upgrade: foo, h2c, bar + + But then following not: + + Connection: upgrade: + Upgrade: foo, h2c + Upgrade: bar + + Then, when a "h2c" or "h2" token is finally reported, all Upgrade headers + are removed, regardless other tokens. + + So, to fix the both issues, everything is now handled during the message + parsing by skipping "h2c" and "h2" tokens, rebuilding the Upgrade header + value without then offending tokens. The same was already performed for the + Connection header, to skip "keep-alive" and "close" value. So it is not a so + fancy change. + + Thanks to this change, it is no longer necessary to handle H1_MF_UPG_H2C + during the request post-parsing. And in fact, this flag is no longer + necessary. So let's remove it too. + + Thanks to Vincent55 for finding and reporting this. + + This patch must be backported as far as 2.4. +diff --git a/include/haproxy/h1.h b/include/haproxy/h1.h +index b4e66908e..71579e030 100644 +--- a/include/haproxy/h1.h ++++ b/include/haproxy/h1.h +@@ -98,7 +98,7 @@ enum h1m_state { + #define H1_MF_UPG_WEBSOCKET 0x00008000 // Set for a Websocket upgrade handshake + #define H1_MF_TE_CHUNKED 0x00010000 // T-E "chunked" + #define H1_MF_TE_OTHER 0x00020000 // T-E other than supported ones found (only "chunked" is supported for now) +-#define H1_MF_UPG_H2C 0x00040000 // "h2c" or "h2" used as upgrade token ++/* unused: 0x00040000 */ + + /* Mask to use to reset H1M flags when we restart headers parsing. + * +@@ -160,7 +160,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + int h1_parse_cont_len_header(struct h1m *h1m, struct ist *value); + int h1_parse_xfer_enc_header(struct h1m *h1m, struct ist value); + void h1_parse_connection_header(struct h1m *h1m, struct ist *value); +-void h1_parse_upgrade_header(struct h1m *h1m, struct ist value); ++void h1_parse_upgrade_header(struct h1m *h1m, struct ist *value); + + void h1_generate_random_ws_input_key(char key_out[25]); + void h1_calculate_ws_output_key(const char *key, char *result); +diff --git a/src/h1.c b/src/h1.c +index 6b2938023..135e338a3 100644 +--- a/src/h1.c ++++ b/src/h1.c +@@ -368,17 +368,19 @@ void h1_parse_connection_header(struct h1m *h1m, struct ist *value) + + /* Parse the Upgrade: header of an HTTP/1 request. + * If "websocket" is found, set H1_MF_UPG_WEBSOCKET flag +- * If "h2c" or "h2" found, set H1_MF_UPG_H2C flag. ++ * If "h2c" or "h2" found, the value is skipped. + */ +-void h1_parse_upgrade_header(struct h1m *h1m, struct ist value) ++void h1_parse_upgrade_header(struct h1m *h1m, struct ist *value) + { +- char *e, *n; ++ char *e, *n, *p; + struct ist word; + +- h1m->flags &= ~(H1_MF_UPG_WEBSOCKET|H1_MF_UPG_H2C); ++ h1m->flags &= ~H1_MF_UPG_WEBSOCKET; + +- word.ptr = value.ptr - 1; // -1 for next loop's pre-increment +- e = istend(value); ++ word.ptr = value->ptr - 1; // -1 for next loop's pre-increment ++ p = value->ptr; ++ e = value->ptr + value->len; ++ value->len = 0; + + while (++word.ptr < e) { + /* skip leading delimiter and blanks */ +@@ -395,9 +397,20 @@ void h1_parse_upgrade_header(struct h1m *h1m, struct ist value) + if (isteqi(word, ist("websocket"))) + h1m->flags |= H1_MF_UPG_WEBSOCKET; + else if (isteqi(word, ist("h2c")) || isteqi(word, ist("h2"))) +- h1m->flags |= H1_MF_UPG_H2C; ++ goto skip_val; + +- word.ptr = n; ++ if (value->ptr + value->len == p) { ++ /* no rewrite done till now */ ++ value->len = n - value->ptr; ++ } ++ else { ++ if (value->len) ++ value->ptr[value->len++] = ','; ++ istcat(value, word, e - value->ptr); ++ } ++ ++ skip_val: ++ word.ptr = p = n; + } + } + +@@ -1074,7 +1087,11 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + } + } + else if (isteqi(n, ist("upgrade"))) { +- h1_parse_upgrade_header(h1m, v); ++ h1_parse_upgrade_header(h1m, &v); ++ if (!v.len) { ++ /* skip it */ ++ break; ++ } + } + else if (!(h1m->flags & H1_MF_RESP) && isteqi(n, ist("host"))) { + if (host_idx == -1) { +diff --git a/src/h1_htx.c b/src/h1_htx.c +index cc1d646e7..23fd28455 100644 +--- a/src/h1_htx.c ++++ b/src/h1_htx.c +@@ -190,20 +190,6 @@ static int h1_postparse_req_hdrs(struct h1m *h1m, union h1_sl *h1sl, struct htx + + flags |= h1m_htx_sl_flags(h1m); + +- /* Remove Upgrade header in problematic cases : +- * - "h2c" or "h2" token specified as token +- */ +- if ((h1m->flags & (H1_MF_CONN_UPG|H1_MF_UPG_H2C)) == (H1_MF_CONN_UPG|H1_MF_UPG_H2C)) { +- int i; +- +- for (i = 0; hdrs[i].n.len; i++) { +- if (isteqi(hdrs[i].n, ist("upgrade"))) +- hdrs[i].v = IST_NULL; +- } +- h1m->flags &=~ H1_MF_CONN_UPG; +- flags &= ~HTX_SL_F_CONN_UPG; +- } +- + sl = htx_add_stline(htx, HTX_BLK_REQ_SL, flags, meth, uri, vsn); + if (!sl || !htx_add_all_headers(htx, hdrs)) + goto error; +diff --git a/src/mux_h1.c b/src/mux_h1.c +index 1cfbb3697..a38cdaf28 100644 +--- a/src/mux_h1.c ++++ b/src/mux_h1.c +@@ -2519,7 +2519,9 @@ static size_t h1_make_headers(struct h1s *h1s, struct h1m *h1m, struct htx *htx, + goto nextblk; + } + else if (isteq(n, ist("upgrade"))) { +- h1_parse_upgrade_header(h1m, v); ++ h1_parse_upgrade_header(h1m, &v); ++ if (!v.len) ++ goto nextblk; + } + else if ((isteq(n, ist("sec-websocket-accept")) && h1m->flags & H1_MF_RESP) || + (isteq(n, ist("sec-websocket-key")) && !(h1m->flags & H1_MF_RESP))) { diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,92 @@ +From 168ad4d3031d80fbd60ca3823b0914054fcd0d38 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Fri, 16 May 2025 14:58:52 +0200 +Subject: [PATCH] BUG/MEDIUM: h1/h2/h3: reject forbidden chars in the Host + header field + +In continuation with 9a05c1f574 ("BUG/MEDIUM: h2/h3: reject some +forbidden chars in :authority before reassembly") and the discussion +in issue #2941, @DemiMarie rightfully suggested that Host should also +be sanitized, because it is sometimes used in concatenation, such as +this: + + http-request set-url https://%[req.hdr(host)]%[pathq] + +which was proposed as a workaround for h2 upstream servers that require +:authority here: + + https://www.mail-archive.com/haproxy@formilux.org/msg43261.html + +The current patch then adds the same check for forbidden chars in the +Host header, using the same function as for the patch above, since in +both cases we validate the host:port part of the authority. This way +we won't reconstruct ambiguous URIs by concatenating Host and path. + +Just like the patch above, this can be backported afer a period of +observation. + +(cherry picked from commit df00164fdd98d15e832daad34fb23249083bfb9c) +Signed-off-by: Christopher Faulet +(cherry picked from commit 38ef948e754e4cab938f219349642c89cf0a79e6) +Signed-off-by: Christopher Faulet +--- + src/h1.c | 8 +++++++- + src/h2.c | 3 +++ + src/h3.c | 3 ++- + 3 files changed, 12 insertions(+), 2 deletions(-) + +diff --git a/src/h1.c b/src/h1.c +index 09047fd2c8c4..c8c56572197f 100644 +--- a/src/h1.c ++++ b/src/h1.c +@@ -1077,8 +1077,14 @@ int h1_headers_to_hdr_list(char *start, const char *stop, + h1_parse_upgrade_header(h1m, v); + } + else if (!(h1m->flags & H1_MF_RESP) && isteqi(n, ist("host"))) { +- if (host_idx == -1) ++ if (host_idx == -1) { + host_idx = hdr_count; ++ if (http_authority_has_forbidden_char(v)) { ++ state = H1_MSG_HDR_L2_LWS; ++ ptr = v.ptr; /* Set ptr on the error */ ++ goto http_msg_invalid; ++ } ++ } + else { + if (!isteqi(v, hdr[host_idx].v)) { + state = H1_MSG_HDR_L2_LWS; +diff --git a/src/h2.c b/src/h2.c +index 227b7a81bd79..380596affbf5 100644 +--- a/src/h2.c ++++ b/src/h2.c +@@ -411,10 +411,13 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms + } + + if (isteq(list[idx].n, ist("host"))) { ++ /* skip duplicates */ + if (fields & H2_PHDR_FND_HOST) + continue; + + fields |= H2_PHDR_FND_HOST; ++ if (http_authority_has_forbidden_char(list[idx].v)) ++ goto fail; + } + + if (isteq(list[idx].n, ist("content-length"))) { +diff --git a/src/h3.c b/src/h3.c +index b35c7e5ddb7d..8b108a98f3b7 100644 +--- a/src/h3.c ++++ b/src/h3.c +@@ -850,7 +850,8 @@ static ssize_t h3_headers_to_htx(struct qcs *qcs, const struct buffer *buf, + if (isteq(list[hdr_idx].n, ist("host"))) { + struct ist prev_auth = authority; + +- if (h3_set_authority(qcs, &authority, list[hdr_idx].v)) { ++ if (http_authority_has_forbidden_char(list[hdr_idx].v) || ++ h3_set_authority(qcs, &authority, list[hdr_idx].v)) { + h3s->err = H3_ERR_MESSAGE_ERROR; + qcc_report_glitch(h3c->qcc, 1); + len = -1; +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,86 @@ +From a4f2dc0c6dadae483a33806bbea415488b4fbc32 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Mon, 12 May 2025 17:45:33 +0200 +Subject: [PATCH] BUG/MEDIUM: h2/h3: reject some forbidden chars in :authority + before reassembly + +As discussed here: + https://github.com/httpwg/http2-spec/pull/936 + https://github.com/haproxy/haproxy/issues/2941 + +It's important to take care of some special characters in the :authority +pseudo header before reassembling a complete URI, because after assembly +it's too late (e.g. the '/'). This patch does this, both for h2 and h3. + +The impact on H2 was measured in the worst case at 0.3% of the request +rate, while the impact on H3 is around 1%, but H3 was about 1% faster +than H2 before and is now on par. + +It may be backported after a period of observation, and in this case it +relies on this previous commit: + + MINOR: http: add a function to validate characters of :authority + +Thanks to @DemiMarie for reviving this topic in issue #2941 and bringing +new potential interesting cases. + +(cherry picked from commit 9a05c1f57490ba3adb378ad8e6e26830425514e7) +Signed-off-by: Christopher Faulet +(cherry picked from commit 479befa356966c12a0a0ead74971750de4ac4499) +Signed-off-by: Christopher Faulet +--- + src/h2.c | 11 +++++++++++ + src/h3.c | 15 +++++++++++++++ + 2 files changed, 26 insertions(+) + +diff --git a/src/h2.c b/src/h2.c +index e2674323199d..227b7a81bd79 100644 +--- a/src/h2.c ++++ b/src/h2.c +@@ -203,6 +203,17 @@ static struct htx_sl *h2_prepare_htx_reqline(uint32_t fields, struct ist *phdr, + } + } + ++ /* We're going to concatenate :authority with :path to form a URI. Some ++ * characters must absolutely be avoided in :authority to make sure not ++ * to result in a broken concatenation. See the following links for a ++ * discussion on this topic: ++ * https://github.com/httpwg/http2-spec/pull/936 ++ * https://github.com/haproxy/haproxy/issues/2941 ++ */ ++ if ((fields & H2_PHDR_FND_AUTH) && ++ http_authority_has_forbidden_char(phdr[H2_PHDR_IDX_AUTH])) ++ goto fail; ++ + if (!(flags & HTX_SL_F_HAS_SCHM)) { + /* no scheme, use authority only (CONNECT) */ + uri = phdr[H2_PHDR_IDX_AUTH]; +diff --git a/src/h3.c b/src/h3.c +index 27497032aaa8..b35c7e5ddb7d 100644 +--- a/src/h3.c ++++ b/src/h3.c +@@ -757,6 +757,21 @@ static ssize_t h3_headers_to_htx(struct qcs *qcs, const struct buffer *buf, + goto out; + } + ++ /* We're going to concatenate :authority with :path to form a URI. Some ++ * characters must absolutely be avoided in :authority to make sure not ++ * to result in a broken concatenation. See the following links for a ++ * discussion on this topic: ++ * https://github.com/httpwg/http2-spec/pull/936 ++ * https://github.com/haproxy/haproxy/issues/2941 ++ */ ++ if (http_authority_has_forbidden_char(authority)) { ++ TRACE_ERROR("invalid character in authority", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs); ++ h3s->err = H3_ERR_MESSAGE_ERROR; ++ qcc_report_glitch(h3c->qcc, 1); ++ len = -1; ++ goto out; ++ } ++ + /* Ensure that final URI does not contains LWS nor CTL characters. */ + for (i = 0; i < path.len; i++) { + unsigned char c = istptr(path)[i]; +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,49 @@ +From e03a093e4d9ba7ef1ff17b8647adea104f23ac0a Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Mon, 4 May 2026 13:42:52 +0200 +Subject: [PATCH] BUG/MEDIUM: mux-h2: fix the body_len to check when parsing + request trailers + +The h2 content-length validation in commit d12edebe4a ("BUG/MAJOR: +mux-h2: detect incomplete transfers on HEADERS frames as well") was +insufficient. The content-length check is still ineffective on request +trailers and it could not work by default due to the fact that the +default body_len is used in h2c_frt_handle_headers() when processing +trailers, instead of passing h2s->body_len, which was necessarily parsed +before reaching trailers. Let's fix this point first, otherwise fixing +the second issue would break trailers. + +Many thanks to Pratham Gupta / alchemy1729 for spotting and analyzing +this problem, and for providing a lightweight reproducer to illustrate +the problem! + +This fix must be backported to all versions where the fix above was +backported (i.e. all). + +(cherry picked from commit b6995d25d1b694f52d1f23463532680b484c775f) +Signed-off-by: Willy Tarreau +(cherry picked from commit 0f820337af9028b3c6f90d2ddc21f749ed71eb8f) +Signed-off-by: Willy Tarreau +(cherry picked from commit ccff5a0314027763569c4ae19b53aa8f1a3c06ec) +[wt: ctx adj since no h2s_rxbuf_tail() etc in 3.0] +Signed-off-by: Willy Tarreau +--- + src/mux_h2.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/mux_h2.c b/src/mux_h2.c +index bd12e55a8f93..2253286f2982 100644 +--- a/src/mux_h2.c ++++ b/src/mux_h2.c +@@ -2929,7 +2929,7 @@ static struct h2s *h2c_frt_handle_headers(struct h2c *h2c, struct h2s *h2s) + if (h2s->st != H2_SS_IDLE) { + /* The stream exists/existed, this must be a trailers frame */ + if (h2s->st != H2_SS_CLOSED) { +- error = h2c_dec_hdrs(h2c, &h2s->rxbuf, &h2s->flags, &body_len, NULL); ++ error = h2c_dec_hdrs(h2c, &h2s->rxbuf, &h2s->flags, &h2s->body_len, NULL); + /* unrecoverable error ? */ + if (h2c->st0 >= H2_CS_ERROR) { + TRACE_USER("Unrecoverable error decoding H2 trailers", H2_EV_RX_FRAME|H2_EV_RX_HDR|H2_EV_STRM_NEW|H2_EV_STRM_END, h2c->conn, 0, &rxbuf); +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch --- haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,46 @@ +From a7fd00bcb9cb29a6f16fe6c6e0e64da8bc44e583 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Sun, 26 Apr 2026 13:51:16 +0200 +Subject: [PATCH] BUG/MEDIUM: mux_h1: fix stack buffer overflow in + h1_append_chunk_size() + +The char tmp[10] buffer can only hold 8 hex digits + CRLF suffix. If chksz +exceeds 4GB (0xFFFFFFFF), the do-while loop writes more than 8 hex digits, +overflowing the stack buffer by 1+ bytes. In practice the buffer is aligned +from the end and leaves a 6-byte hole before it on 64-bit systems, leaving +enough room to be harmless, and 4 on 32-bit platforms which save it from +touching lower variables. So it is safe but just by luck. + +Fix by increasing tmp[] to 18 bytes, sufficient for up to 16 hex digits +(2^64 - 1) plus CRLF. + +(cherry picked from commit 1ef74fc7cec12eba7daeef0b44f96b1ad4f5e127) +Signed-off-by: Christopher Faulet +(cherry picked from commit 77f6a286fa18236d754f4986711495f36e0a0547) +Signed-off-by: Christopher Faulet +(cherry picked from commit 54a4c294a9013620dfa7c5f7dc15bce27e87035d) +Signed-off-by: Christopher Faulet +--- + src/mux_h1.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/mux_h1.c b/src/mux_h1.c +index 0462f398123f..3bfa2328ecf5 100644 +--- a/src/mux_h1.c ++++ b/src/mux_h1.c +@@ -1707,10 +1707,10 @@ static void h1_prepend_chunk_size(struct buffer *buf, size_t chksz, size_t lengt + */ + static int h1_append_chunk_size(struct buffer *buf, size_t chksz) + { +- char tmp[10]; ++ char tmp[18]; + char *beg, *end; + +- beg = end = tmp+10; ++ beg = end = tmp+sizeof(tmp); + *--beg = '\n'; + *--beg = '\r'; + do { +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/0001-MINOR-http-add-a-function-to-validate-characters-of-.patch haproxy-3.0.11/debian/patches/0001-MINOR-http-add-a-function-to-validate-characters-of-.patch --- haproxy-3.0.11/debian/patches/0001-MINOR-http-add-a-function-to-validate-characters-of-.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-3.0.11/debian/patches/0001-MINOR-http-add-a-function-to-validate-characters-of-.patch 2026-05-20 19:21:22.000000000 +0000 @@ -0,0 +1,97 @@ +From 4e4c4baef67cae8138cde2e86ec450a9d24e83a9 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Mon, 12 May 2025 17:39:08 +0200 +Subject: [PATCH] MINOR: http: add a function to validate characters of + :authority + +As discussed here: + https://github.com/httpwg/http2-spec/pull/936 + https://github.com/haproxy/haproxy/issues/2941 + +It's important to take care of some special characters in the :authority +pseudo header before reassembling a complete URI, because after assembly +it's too late (e.g. the '/'). + +This patch adds a specific function which was checks all such characters +and their ranges on an ist, and benefits from modern compilers +optimizations that arrange the comparisons into an evaluation tree for +faster match. That's the version that gave the most consistent performance +across various compilers, though some hand-crafted versions using bitmaps +stored in register could be slightly faster but super sensitive to code +ordering, suggesting that the results might vary with future compilers. +This one takes on average 1.2ns per character at 3 GHz (3.6 cycles per +char on avg). The resulting impact on H2 request processing time (small +requests) was measured around 0.3%, from 6.60 to 6.618us per request, +which is a bit high but remains acceptable given that the test only +focused on req rate. + +The code was made usable both for H2 and H3. + +(cherry picked from commit ebab479cdf34255cd6162d2e843645f88b95327f) +Signed-off-by: Christopher Faulet +(cherry picked from commit dcb963f9d777af39926e83f20e0a3c65c54f3bc0) +Signed-off-by: Christopher Faulet +--- + include/haproxy/http.h | 46 ++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) + +diff --git a/include/haproxy/http.h b/include/haproxy/http.h +index f7f804d7dd58..af1bb3cf24c8 100644 +--- a/include/haproxy/http.h ++++ b/include/haproxy/http.h +@@ -232,6 +232,52 @@ static inline int http_path_has_forbidden_char(const struct ist ist, const char + return 0; + } + ++/* Checks whether the :authority pseudo header contains dangerous chars that ++ * might affect its reassembly. We want to catch anything below 0x21, above ++ * 0x7e, as well as '@', '[', ']', '/','?', '#', '\', CR, LF, NUL. Then we ++ * fall back to the slow path and decide. Brackets are used for IP-literal and ++ * deserve special case, that is better handled in the slow path. The function ++ * returns 0 if no forbidden char is presnet, non-zero otherwise. ++ */ ++static inline int http_authority_has_forbidden_char(const struct ist ist) ++{ ++ size_t ofs, len = istlen(ist); ++ const char *p = istptr(ist); ++ int brackets = 0; ++ uchar c; ++ ++ /* Many attempts with various methods have shown that moderately recent ++ * compilers (gcc >= 9, clang >= 13) will arrange the code below as an ++ * evaluation tree that remains efficient at -O2 and above (~1.2ns per ++ * char). The immediate next efficient one is the bitmap from 64-bit ++ * registers but it's extremely sensitive to code arrangements and ++ * optimization. ++ */ ++ for (ofs = 0; ofs < len; ofs++) { ++ c = p[ofs]; ++ ++ if (unlikely(c < 0x21 || c > 0x7e || ++ c == '#' || c == '/' || c == '?' || c == '@' || ++ c == '[' || c == '\\' || c == ']')) { ++ /* all of them must be rejected, except '[' which may ++ * only appear at the beginning, and ']' which may ++ * only appear at the end or before a colon. ++ */ ++ if ((c == '[' && ofs == 0) || ++ (c == ']' && (ofs == len - 1 || p[ofs + 1] == ':'))) { ++ /* that's an IP-literal (see RFC3986#3.2), it's ++ * OK for now. ++ */ ++ brackets ^= 1; ++ } else { ++ return 1; ++ } ++ } ++ } ++ /* there must be no opening bracket left nor lone closing one */ ++ return brackets; ++} ++ + /* Checks status code array for the presence of status code . + * Returns non-zero if the code is present, zero otherwise. Any status code is + * permitted. +-- +2.53.0 + diff -Nru haproxy-3.0.11/debian/patches/series haproxy-3.0.11/debian/patches/series --- haproxy-3.0.11/debian/patches/series 2026-02-11 21:52:43.000000000 +0000 +++ haproxy-3.0.11/debian/patches/series 2026-05-20 19:21:22.000000000 +0000 @@ -5,6 +5,23 @@ cross.patch 0001-BUG-CRITICAL-mjson-fix-possible-DoS-when-parsing-num.patch 0001-BUG-MAJOR-quic-reject-invalid-token.patch +0001-BUG-MAJOR-h3-check-body-size-with-content-length-on-.patch +0001-BUG-MAJOR-slz-always-make-sure-to-limit-fixed-output.patch +0001-BUG-MAJOR-mux-h2-detect-incomplete-transfers-on-HEAD.patch + +0001-BUG-MEDIUM-mux_h1-fix-stack-buffer-overflow-in-h1_ap.patch +0001-BUG-MAJOR-mux-h1-Deal-with-true-64-bits-integer-to-e.patch + +0001-BUG-MEDIUM-mux-h2-fix-the-body_len-to-check-when-par.patch +0001-BUG-MAJOR-mux-h2-preset-MSGF_BODY_CL-on-H2_SF_DATA_C.patch + +0001-BUG-MEDIUM-h1-Enforce-the-authority-validation-durin.patch +0001-MINOR-http-add-a-function-to-validate-characters-of-.patch +0001-BUG-MEDIUM-h2-h3-reject-some-forbidden-chars-in-auth.patch +0001-BUG-MEDIUM-h1-h2-h3-reject-forbidden-chars-in-the-Ho.patch +0001-BUG-MAJOR-http-forbid-comma-character-in-authority-v.patch + +0001-BUG-MEDIUM-h1-Skip-all-h2c-values-from-Upgrade-heade.patch # applied during the build process: # debianize-dconv.patch