parent
c7329cd393
commit
31fa894416
@ -0,0 +1,52 @@
|
|||||||
|
From 2c681c6f30fb90adab4701e287ff7a7db669b2e7 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Christopher Faulet <cfaulet@haproxy.com>
|
||||||
|
Date: Thu, 22 Dec 2022 09:47:01 +0100
|
||||||
|
Subject: [PATCH] BUG/MEDIUM: mux-h2: Refuse interim responses with end-stream
|
||||||
|
flag set
|
||||||
|
|
||||||
|
As state in RFC9113#8.1, HEADERS frame with the ES flag set that carries an
|
||||||
|
informational status code is malformed. However, there is no test on this
|
||||||
|
condition.
|
||||||
|
|
||||||
|
On 2.4 and higher, it is hard to predict consequences of this bug because
|
||||||
|
end of the message is only reported with a flag. But on 2.2 and lower, it
|
||||||
|
leads to a crash because there is an unexpected extra EOM block at the end
|
||||||
|
of an interim response.
|
||||||
|
|
||||||
|
Now, when a ES flag is detected on a HEADERS frame for an interim message, a
|
||||||
|
stream error is sent (RST_STREAM/PROTOCOL_ERROR).
|
||||||
|
|
||||||
|
This patch should solve the issue #1972. It should be backported as far as
|
||||||
|
2.0.
|
||||||
|
|
||||||
|
(cherry picked from commit 827a6299e6995c5c3ba620d8b7cbacdaef67f2c4)
|
||||||
|
Signed-off-by: Willy Tarreau <w@1wt.eu>
|
||||||
|
(cherry picked from commit ebfae006c6b5de1d1fe0cdd51847ec1e39d5cf59)
|
||||||
|
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
|
||||||
|
(cherry picked from commit 84f5cba24f59b1c8339bb38323fcb01f434ba8e5)
|
||||||
|
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
|
||||||
|
(cherry picked from commit f5748a98c34bc889cae9386ca4f7073ab3f4c6b1)
|
||||||
|
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
|
||||||
|
---
|
||||||
|
src/mux_h2.c | 5 +++++
|
||||||
|
1 file changed, 5 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/src/mux_h2.c b/src/mux_h2.c
|
||||||
|
index 7d23e5abd..7dbbfefec 100644
|
||||||
|
--- a/src/mux_h2.c
|
||||||
|
+++ b/src/mux_h2.c
|
||||||
|
@@ -4942,6 +4942,11 @@ static int h2c_decode_headers(struct h2c *h2c, struct buffer *rxbuf, uint32_t *f
|
||||||
|
*flags |= H2_SF_HEADERS_RCVD;
|
||||||
|
|
||||||
|
if (h2c->dff & H2_F_HEADERS_END_STREAM) {
|
||||||
|
+ if (msgf & H2_MSGF_RSP_1XX) {
|
||||||
|
+ /* RFC9113#8.1 : HEADERS frame with the ES flag set that carries an informational status code is malformed */
|
||||||
|
+ 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;
|
||||||
|
+ }
|
||||||
|
/* no more data are expected for this message */
|
||||||
|
htx->flags |= HTX_FL_EOM;
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.37.3
|
||||||
|
|
@ -0,0 +1,164 @@
|
|||||||
|
From 486cd730485c8a119ef65b3f792134b56e7941b4 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Willy Tarreau <w@1wt.eu>
|
||||||
|
Date: Thu, 9 Feb 2023 21:36:54 +0100
|
||||||
|
Subject: [PATCH] BUG/CRITICAL: http: properly reject empty http header field
|
||||||
|
names
|
||||||
|
|
||||||
|
The HTTP header parsers surprizingly accepts empty header field names,
|
||||||
|
and this is a leftover from the original code that was agnostic to this.
|
||||||
|
|
||||||
|
When muxes were introduced, for H2 first, the HPACK decompressor needed
|
||||||
|
to feed headers lists, and since empty header names were strictly
|
||||||
|
forbidden by the protocol, the lists of headers were purposely designed
|
||||||
|
to be terminated by an empty header field name (a principle that is
|
||||||
|
similar to H1's empty line termination). This principle was preserved
|
||||||
|
and generalized to other protocols migrated to muxes (H1/FCGI/H3 etc)
|
||||||
|
without anyone ever noticing that the H1 parser was still able to deliver
|
||||||
|
empty header field names to this list. In addition to this it turns out
|
||||||
|
that the HPACK decompressor, despite a comment in the code, may
|
||||||
|
successfully decompress an empty header field name, and this mistake
|
||||||
|
was propagated to the QPACK decompressor as well.
|
||||||
|
|
||||||
|
The impact is that an empty header field name may be used to truncate
|
||||||
|
the list of headers and thus make some headers disappear. While for
|
||||||
|
H2/H3 the impact is limited as haproxy sees a request with missing
|
||||||
|
headers, and headers are not used to delimit messages, in the case of
|
||||||
|
HTTP/1, the impact is significant because the presence (and sometimes
|
||||||
|
contents) of certain sensitive headers is detected during the parsing.
|
||||||
|
Thus, some of these headers may be seen, marked as present, their value
|
||||||
|
extracted, but never delivered to upper layers and obviously not
|
||||||
|
forwarded to the other side either. This can have for consequence that
|
||||||
|
certain important header fields such as Connection, Upgrade, Host,
|
||||||
|
Content-length, Transfer-Encoding etc are possibly seen as different
|
||||||
|
between what haproxy uses to parse/forward/route and what is observed
|
||||||
|
in http-request rules and of course, forwarded. One direct consequence
|
||||||
|
is that it is possible to exploit this property in HTTP/1 to make
|
||||||
|
affected versions of haproxy forward more data than is advertised on
|
||||||
|
the other side, and bypass some access controls or routing rules by
|
||||||
|
crafting extraneous requests. Note, however, that responses to such
|
||||||
|
requests will normally not be passed back to the client, but this can
|
||||||
|
still cause some harm.
|
||||||
|
|
||||||
|
This specific risk can be mostly worked around in configuration using
|
||||||
|
the following rule that will rely on the bug's impact to precisely
|
||||||
|
detect the inconsistency between the known body size and the one
|
||||||
|
expected to be advertised to the server (the rule works from 2.0 to
|
||||||
|
2.8-dev):
|
||||||
|
|
||||||
|
http-request deny if { fc_http_major 1 } !{ req.body_size 0 } !{ req.hdr(content-length) -m found } !{ req.hdr(transfer-encoding) -m found } !{ method CONNECT }
|
||||||
|
|
||||||
|
This will exclusively block such carefully crafted requests delivered
|
||||||
|
over HTTP/1. HTTP/2 and HTTP/3 do not need content-length, and a body
|
||||||
|
that arrives without being announced with a content-length will be
|
||||||
|
forwarded using transfer-encoding, hence will not cause discrepancies.
|
||||||
|
In HAProxy 2.0 in legacy mode ("no option http-use-htx"), this rule will
|
||||||
|
simply have no effect but will not cause trouble either.
|
||||||
|
|
||||||
|
A clean solution would consist in modifying the loops iterating over
|
||||||
|
these headers lists to check the header name's pointer instead of its
|
||||||
|
length (since both are zero at the end of the list), but this requires
|
||||||
|
to touch tens of places and it's very easy to miss one. Functions such
|
||||||
|
as htx_add_header(), htx_add_trailer(), htx_add_all_headers() would be
|
||||||
|
good starting points for such a possible future change.
|
||||||
|
|
||||||
|
Instead the current fix focuses on blocking empty headers where they
|
||||||
|
are first inserted, hence in the H1/HPACK/QPACK decoders. One benefit
|
||||||
|
of the current solution (for H1) is that it allows "show errors" to
|
||||||
|
report a precise diagnostic when facing such invalid HTTP/1 requests,
|
||||||
|
with the exact location of the problem and the originating address:
|
||||||
|
|
||||||
|
$ printf "GET / HTTP/1.1\r\nHost: localhost\r\n:empty header\r\n\r\n" | nc 0 8001
|
||||||
|
HTTP/1.1 400 Bad request
|
||||||
|
Content-length: 90
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Connection: close
|
||||||
|
Content-Type: text/html
|
||||||
|
|
||||||
|
<html><body><h1>400 Bad request</h1>
|
||||||
|
Your browser sent an invalid request.
|
||||||
|
</body></html>
|
||||||
|
|
||||||
|
$ socat /var/run/haproxy.stat <<< "show errors"
|
||||||
|
Total events captured on [10/Feb/2023:16:29:37.530] : 1
|
||||||
|
|
||||||
|
[10/Feb/2023:16:29:34.155] frontend decrypt (#2): invalid request
|
||||||
|
backend <NONE> (#-1), server <NONE> (#-1), event #0, src 127.0.0.1:31092
|
||||||
|
buffer starts at 0 (including 0 out), 16334 free,
|
||||||
|
len 50, wraps at 16336, error at position 33
|
||||||
|
H1 connection flags 0x00000000, H1 stream flags 0x00000810
|
||||||
|
H1 msg state MSG_HDR_NAME(17), H1 msg flags 0x00001410
|
||||||
|
H1 chunk len 0 bytes, H1 body len 0 bytes :
|
||||||
|
|
||||||
|
00000 GET / HTTP/1.1\r\n
|
||||||
|
00016 Host: localhost\r\n
|
||||||
|
00033 :empty header\r\n
|
||||||
|
00048 \r\n
|
||||||
|
|
||||||
|
I want to address sincere and warm thanks for their great work to the
|
||||||
|
team composed of the following security researchers who found the issue
|
||||||
|
together and reported it: Bahruz Jabiyev, Anthony Gavazzi, and Engin
|
||||||
|
Kirda from Northeastern University, Kaan Onarlioglu from Akamai
|
||||||
|
Technologies, Adi Peleg and Harvey Tuch from Google. And kudos to Amaury
|
||||||
|
Denoyelle from HAProxy Technologies for spotting that the HPACK and
|
||||||
|
QPACK decoders would let this pass despite the comment explicitly
|
||||||
|
saying otherwise.
|
||||||
|
|
||||||
|
This fix must be backported as far as 2.0. The QPACK changes can be
|
||||||
|
dropped before 2.6. In 2.0 there is also the equivalent code for legacy
|
||||||
|
mode, which doesn't suffer from the list truncation, but it would better
|
||||||
|
be fixed regardless.
|
||||||
|
|
||||||
|
CVE-2023-25725 was assigned to this issue.
|
||||||
|
|
||||||
|
(cherry picked from commit a8598a2eb11b6c989e81f0dbf10be361782e8d32)
|
||||||
|
Signed-off-by: Willy Tarreau <w@1wt.eu>
|
||||||
|
(cherry picked from commit a0e561ad7f29ed50c473f5a9da664267b60d1112)
|
||||||
|
Signed-off-by: Willy Tarreau <w@1wt.eu>
|
||||||
|
(cherry picked from commit 73be199c4f5f1ed468161a4c5e10ca77cd5989d8)
|
||||||
|
[wt: dropped QPACK changes for 2.5]
|
||||||
|
Signed-off-by: Willy Tarreau <w@1wt.eu>
|
||||||
|
(cherry picked from commit f8b2b88aeae15dc3b261cd3749277ae75caf9db8)
|
||||||
|
Signed-off-by: Willy Tarreau <w@1wt.eu>
|
||||||
|
---
|
||||||
|
src/h1.c | 4 ++++
|
||||||
|
src/hpack-dec.c | 9 +++++++++
|
||||||
|
2 files changed, 13 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/src/h1.c b/src/h1.c
|
||||||
|
index 4c2e234c5..73de48be0 100644
|
||||||
|
--- a/src/h1.c
|
||||||
|
+++ b/src/h1.c
|
||||||
|
@@ -750,6 +750,10 @@ int h1_headers_to_hdr_list(char *start, const char *stop,
|
||||||
|
|
||||||
|
if (likely(*ptr == ':')) {
|
||||||
|
col = ptr - start;
|
||||||
|
+ if (col <= sol) {
|
||||||
|
+ state = H1_MSG_HDR_NAME;
|
||||||
|
+ goto http_msg_invalid;
|
||||||
|
+ }
|
||||||
|
EAT_AND_JUMP_OR_RETURN(ptr, end, http_msg_hdr_l1_sp, http_msg_ood, state, H1_MSG_HDR_L1_SP);
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/src/hpack-dec.c b/src/hpack-dec.c
|
||||||
|
index 04f3d9ffa..ed39007d1 100644
|
||||||
|
--- a/src/hpack-dec.c
|
||||||
|
+++ b/src/hpack-dec.c
|
||||||
|
@@ -420,6 +420,15 @@ int hpack_decode_frame(struct hpack_dht *dht, const uint8_t *raw, uint32_t len,
|
||||||
|
/* <name> and <value> are correctly filled here */
|
||||||
|
}
|
||||||
|
|
||||||
|
+ /* We must not accept empty header names (forbidden by the spec and used
|
||||||
|
+ * as a list termination).
|
||||||
|
+ */
|
||||||
|
+ if (!name.len) {
|
||||||
|
+ hpack_debug_printf("##ERR@%d##\n", __LINE__);
|
||||||
|
+ ret = -HPACK_ERR_INVALID_ARGUMENT;
|
||||||
|
+ goto leave;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
/* here's what we have here :
|
||||||
|
* - name.len > 0
|
||||||
|
* - value is filled with either const data or data allocated from tmp
|
||||||
|
--
|
||||||
|
2.37.3
|
||||||
|
|
Loading…
Reference in new issue