a590c8
From 486cd730485c8a119ef65b3f792134b56e7941b4 Mon Sep 17 00:00:00 2001
a590c8
From: Willy Tarreau <w@1wt.eu>
a590c8
Date: Thu, 9 Feb 2023 21:36:54 +0100
a590c8
Subject: [PATCH] BUG/CRITICAL: http: properly reject empty http header field
a590c8
 names
a590c8
a590c8
The HTTP header parsers surprizingly accepts empty header field names,
a590c8
and this is a leftover from the original code that was agnostic to this.
a590c8
a590c8
When muxes were introduced, for H2 first, the HPACK decompressor needed
a590c8
to feed headers lists, and since empty header names were strictly
a590c8
forbidden by the protocol, the lists of headers were purposely designed
a590c8
to be terminated by an empty header field name (a principle that is
a590c8
similar to H1's empty line termination). This principle was preserved
a590c8
and generalized to other protocols migrated to muxes (H1/FCGI/H3 etc)
a590c8
without anyone ever noticing that the H1 parser was still able to deliver
a590c8
empty header field names to this list. In addition to this it turns out
a590c8
that the HPACK decompressor, despite a comment in the code, may
a590c8
successfully decompress an empty header field name, and this mistake
a590c8
was propagated to the QPACK decompressor as well.
a590c8
a590c8
The impact is that an empty header field name may be used to truncate
a590c8
the list of headers and thus make some headers disappear. While for
a590c8
H2/H3 the impact is limited as haproxy sees a request with missing
a590c8
headers, and headers are not used to delimit messages, in the case of
a590c8
HTTP/1, the impact is significant because the presence (and sometimes
a590c8
contents) of certain sensitive headers is detected during the parsing.
a590c8
Thus, some of these headers may be seen, marked as present, their value
a590c8
extracted, but never delivered to upper layers and obviously not
a590c8
forwarded to the other side either. This can have for consequence that
a590c8
certain important header fields such as Connection, Upgrade, Host,
a590c8
Content-length, Transfer-Encoding etc are possibly seen as different
a590c8
between what haproxy uses to parse/forward/route and what is observed
a590c8
in http-request rules and of course, forwarded. One direct consequence
a590c8
is that it is possible to exploit this property in HTTP/1 to make
a590c8
affected versions of haproxy forward more data than is advertised on
a590c8
the other side, and bypass some access controls or routing rules by
a590c8
crafting extraneous requests.  Note, however, that responses to such
a590c8
requests will normally not be passed back to the client, but this can
a590c8
still cause some harm.
a590c8
a590c8
This specific risk can be mostly worked around in configuration using
a590c8
the following rule that will rely on the bug's impact to precisely
a590c8
detect the inconsistency between the known body size and the one
a590c8
expected to be advertised to the server (the rule works from 2.0 to
a590c8
2.8-dev):
a590c8
a590c8
  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 }
a590c8
a590c8
This will exclusively block such carefully crafted requests delivered
a590c8
over HTTP/1. HTTP/2 and HTTP/3 do not need content-length, and a body
a590c8
that arrives without being announced with a content-length will be
a590c8
forwarded using transfer-encoding, hence will not cause discrepancies.
a590c8
In HAProxy 2.0 in legacy mode ("no option http-use-htx"), this rule will
a590c8
simply have no effect but will not cause trouble either.
a590c8
a590c8
A clean solution would consist in modifying the loops iterating over
a590c8
these headers lists to check the header name's pointer instead of its
a590c8
length (since both are zero at the end of the list), but this requires
a590c8
to touch tens of places and it's very easy to miss one. Functions such
a590c8
as htx_add_header(), htx_add_trailer(), htx_add_all_headers() would be
a590c8
good starting points for such a possible future change.
a590c8
a590c8
Instead the current fix focuses on blocking empty headers where they
a590c8
are first inserted, hence in the H1/HPACK/QPACK decoders. One benefit
a590c8
of the current solution (for H1) is that it allows "show errors" to
a590c8
report a precise diagnostic when facing such invalid HTTP/1 requests,
a590c8
with the exact location of the problem and the originating address:
a590c8
a590c8
  $ printf "GET / HTTP/1.1\r\nHost: localhost\r\n:empty header\r\n\r\n" | nc 0 8001
a590c8
  HTTP/1.1 400 Bad request
a590c8
  Content-length: 90
a590c8
  Cache-Control: no-cache
a590c8
  Connection: close
a590c8
  Content-Type: text/html
a590c8
a590c8
  <html><body>

400 Bad request

a590c8
  Your browser sent an invalid request.
a590c8
  </body></html>
a590c8
a590c8
  $ socat /var/run/haproxy.stat <<< "show errors"
a590c8
  Total events captured on [10/Feb/2023:16:29:37.530] : 1
a590c8
a590c8
  [10/Feb/2023:16:29:34.155] frontend decrypt (#2): invalid request
a590c8
    backend <NONE> (#-1), server <NONE> (#-1), event #0, src 127.0.0.1:31092
a590c8
    buffer starts at 0 (including 0 out), 16334 free,
a590c8
    len 50, wraps at 16336, error at position 33
a590c8
    H1 connection flags 0x00000000, H1 stream flags 0x00000810
a590c8
    H1 msg state MSG_HDR_NAME(17), H1 msg flags 0x00001410
a590c8
    H1 chunk len 0 bytes, H1 body len 0 bytes :
a590c8
a590c8
    00000  GET / HTTP/1.1\r\n
a590c8
    00016  Host: localhost\r\n
a590c8
    00033  :empty header\r\n
a590c8
    00048  \r\n
a590c8
a590c8
I want to address sincere and warm thanks for their great work to the
a590c8
team composed of the following security researchers who found the issue
a590c8
together and reported it: Bahruz Jabiyev, Anthony Gavazzi, and Engin
a590c8
Kirda from Northeastern University, Kaan Onarlioglu from Akamai
a590c8
Technologies, Adi Peleg and Harvey Tuch from Google. And kudos to Amaury
a590c8
Denoyelle from HAProxy Technologies for spotting that the HPACK and
a590c8
QPACK decoders would let this pass despite the comment explicitly
a590c8
saying otherwise.
a590c8
a590c8
This fix must be backported as far as 2.0. The QPACK changes can be
a590c8
dropped before 2.6. In 2.0 there is also the equivalent code for legacy
a590c8
mode, which doesn't suffer from the list truncation, but it would better
a590c8
be fixed regardless.
a590c8
a590c8
CVE-2023-25725 was assigned to this issue.
a590c8
a590c8
(cherry picked from commit a8598a2eb11b6c989e81f0dbf10be361782e8d32)
a590c8
Signed-off-by: Willy Tarreau <w@1wt.eu>
a590c8
(cherry picked from commit a0e561ad7f29ed50c473f5a9da664267b60d1112)
a590c8
Signed-off-by: Willy Tarreau <w@1wt.eu>
a590c8
(cherry picked from commit 73be199c4f5f1ed468161a4c5e10ca77cd5989d8)
a590c8
[wt: dropped QPACK changes for 2.5]
a590c8
Signed-off-by: Willy Tarreau <w@1wt.eu>
a590c8
(cherry picked from commit f8b2b88aeae15dc3b261cd3749277ae75caf9db8)
a590c8
Signed-off-by: Willy Tarreau <w@1wt.eu>
a590c8
---
a590c8
 src/h1.c        | 4 ++++
a590c8
 src/hpack-dec.c | 9 +++++++++
a590c8
 2 files changed, 13 insertions(+)
a590c8
a590c8
diff --git a/src/h1.c b/src/h1.c
a590c8
index 4c2e234c5..73de48be0 100644
a590c8
--- a/src/h1.c
a590c8
+++ b/src/h1.c
a590c8
@@ -750,6 +750,10 @@ int h1_headers_to_hdr_list(char *start, const char *stop,
a590c8
 
a590c8
 		if (likely(*ptr == ':')) {
a590c8
 			col = ptr - start;
a590c8
+			if (col <= sol) {
a590c8
+				state = H1_MSG_HDR_NAME;
a590c8
+				goto http_msg_invalid;
a590c8
+			}
a590c8
 			EAT_AND_JUMP_OR_RETURN(ptr, end, http_msg_hdr_l1_sp, http_msg_ood, state, H1_MSG_HDR_L1_SP);
a590c8
 		}
a590c8
 
a590c8
diff --git a/src/hpack-dec.c b/src/hpack-dec.c
a590c8
index 04f3d9ffa..ed39007d1 100644
a590c8
--- a/src/hpack-dec.c
a590c8
+++ b/src/hpack-dec.c
a590c8
@@ -420,6 +420,15 @@ int hpack_decode_frame(struct hpack_dht *dht, const uint8_t *raw, uint32_t len,
a590c8
 			/* <name> and <value> are correctly filled here */
a590c8
 		}
a590c8
 
a590c8
+		/* We must not accept empty header names (forbidden by the spec and used
a590c8
+		 * as a list termination).
a590c8
+		 */
a590c8
+		if (!name.len) {
a590c8
+			hpack_debug_printf("##ERR@%d##\n", __LINE__);
a590c8
+			ret = -HPACK_ERR_INVALID_ARGUMENT;
a590c8
+			goto leave;
a590c8
+		}
a590c8
+
a590c8
 		/* here's what we have here :
a590c8
 		 *   - name.len > 0
a590c8
 		 *   - value is filled with either const data or data allocated from tmp
a590c8
-- 
a590c8
2.37.3
a590c8