Version in base suite: 1.8.19-1 Base version: haproxy_1.8.19-1 Target version: haproxy_1.8.19-1+deb10u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/h/haproxy/haproxy_1.8.19-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/h/haproxy/haproxy_1.8.19-1+deb10u1.dsc changelog | 7 gbp.conf | 3 patches/0001-MINOR-ist-add-ist_find_ctl.patch | 76 +++++++ patches/0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch | 108 ++++++++++ patches/0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch | 79 +++++++ patches/series | 5 6 files changed, 277 insertions(+), 1 deletion(-) diff: /srv/release.debian.org/tmp/YS5O_lxpdh/haproxy-1.8.19/reg-tests/lua/common.pem: No such file or directory diff: /srv/release.debian.org/tmp/k4W7uVyE6z/haproxy-1.8.19/reg-tests/lua/common.pem: No such file or directory diff -Nru haproxy-1.8.19/debian/changelog haproxy-1.8.19/debian/changelog --- haproxy-1.8.19/debian/changelog 2019-02-12 08:30:54.000000000 +0000 +++ haproxy-1.8.19/debian/changelog 2019-11-27 16:07:57.000000000 +0000 @@ -1,3 +1,10 @@ +haproxy (1.8.19-1+deb10u1) buster-security; urgency=high + + * Apply two patches around HTTP/2 header validation allowing an attacker + to use a CRLF inside an HTTP header. Fix CVE-2019-19330. + + -- Vincent Bernat Wed, 27 Nov 2019 17:07:57 +0100 + haproxy (1.8.19-1) unstable; urgency=medium * New upstream version 1.8.19 diff -Nru haproxy-1.8.19/debian/gbp.conf haproxy-1.8.19/debian/gbp.conf --- haproxy-1.8.19/debian/gbp.conf 2019-01-14 18:49:40.000000000 +0000 +++ haproxy-1.8.19/debian/gbp.conf 2019-11-27 16:07:57.000000000 +0000 @@ -1,4 +1,5 @@ [DEFAULT] +dist = buster pristine-tar = True upstream-branch = upstream-1.8 -debian-branch = master +ignore-branch = True diff -Nru haproxy-1.8.19/debian/patches/0001-MINOR-ist-add-ist_find_ctl.patch haproxy-1.8.19/debian/patches/0001-MINOR-ist-add-ist_find_ctl.patch --- haproxy-1.8.19/debian/patches/0001-MINOR-ist-add-ist_find_ctl.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-1.8.19/debian/patches/0001-MINOR-ist-add-ist_find_ctl.patch 2019-11-27 16:07:57.000000000 +0000 @@ -0,0 +1,76 @@ +From b23455c68f4a5d9b9ecc859330642514549a2c68 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Fri, 22 Nov 2019 15:58:53 +0100 +Subject: MINOR: ist: add ist_find_ctl() + +This new function looks for the first control character in a string (a +char whose value is between 0x00 and 0x1F included) and returns it, or +NULL if there is none. It is optimized for quickly evicting non-matching +strings and scans ~0.43 bytes per cycle. It can be used as an accelerator +when it's needed to look up several of these characters (e.g. CR/LF/NUL). + +(cherry picked from commit 8f3ce06f14e13719c9353794d60001eab8d43717) +Signed-off-by: Willy Tarreau +(cherry picked from commit 658e0ddd64100874193a1c543cc191e540a277f0) +Signed-off-by: Willy Tarreau +(cherry picked from commit 6a9ffa70ecd747993505236557140a7081b65ee2) +Signed-off-by: Willy Tarreau +--- + include/common/ist.h | 41 +++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 41 insertions(+) + +diff --git a/include/common/ist.h b/include/common/ist.h +index bd98ab0..986e1df 100644 +--- a/include/common/ist.h ++++ b/include/common/ist.h +@@ -366,6 +366,47 @@ static inline char *istchr(const struct ist ist, char chr) + return s - 1; + } + ++/* Returns a pointer to the first control character found in , or NULL if ++ * none is present. A control character is defined as a byte whose value is ++ * between 0x00 and 0x1F included. The function is optimized for strings having ++ * no CTL chars by processing up to sizeof(long) bytes at once on architectures ++ * supporting efficient unaligned accesses. Despite this it is not very fast ++ * (~0.43 byte/cycle) and should mostly be used on low match probability when ++ * it can save a call to a much slower function. ++ */ ++static inline const char *ist_find_ctl(const struct ist ist) ++{ ++ const union { unsigned long v; } __attribute__((packed)) *u; ++ const char *curr = (void *)ist.ptr - sizeof(long); ++ const char *last = curr + ist.len; ++ unsigned long l1, l2; ++ ++ do { ++ curr += sizeof(long); ++ if (curr > last) ++ break; ++ u = (void *)curr; ++ /* subtract 0x202020...20 to the value to generate a carry in ++ * the lower byte if the byte contains a lower value. If we ++ * generate a bit 7 that was not there, it means the byte was ++ * within 0x00..0x1F. ++ */ ++ l2 = u->v; ++ l1 = ~l2 & ((~0UL / 255) * 0x80); /* 0x808080...80 */ ++ l2 -= (~0UL / 255) * 0x20; /* 0x202020...20 */ ++ } while ((l1 & l2) == 0); ++ ++ last += sizeof(long); ++ if (__builtin_expect(curr < last, 0)) { ++ do { ++ if ((uint8_t)*curr < 0x20) ++ return curr; ++ curr++; ++ } while (curr < last); ++ } ++ return NULL; ++} ++ + /* looks for first occurrence of character in string and returns + * the tail of the string starting with this character, or (ist.end,0) if not + * found. +-- +2.9.0 + diff -Nru haproxy-1.8.19/debian/patches/0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch haproxy-1.8.19/debian/patches/0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch --- haproxy-1.8.19/debian/patches/0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-1.8.19/debian/patches/0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch 2019-11-27 16:07:57.000000000 +0000 @@ -0,0 +1,108 @@ +From b8d65bb1f52849665ef6f21d90ec5fc3b7c00bc6 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Fri, 22 Nov 2019 16:02:43 +0100 +Subject: BUG/MAJOR: h2: reject header values containing invalid chars +MIME-Version: 1.0 +Content-Type: text/plain; charset=latin1 +Content-Transfer-Encoding: 8bit + +Tim Düsterhus reported an annoying problem in the H2 decoder related to +an ambiguity in the H2 spec. The spec says in section 10.3 that HTTP/2 +allows header field values that are not valid (since they're binary) and +at the same time that an H2 to H1 gateway must be careful to reject headers +whose values contain \0, \r or \n. + +Till now, and for the sake of the ability to maintain end-to-end binary +transparency in H2-to-H2, the H2 mux wouldn't reject this since it does +not know what version will be used on the other side. + +In theory we should in fact perform such a check when converting an HTX +header to H1. But this causes a problem as it means that all our rule sets, +sample fetches, captures, logs or redirects may still find an LF in a header +coming from H2. Also in 2.0 and older in legacy mode, the frames are instantly +converted to H1 and HTX couldn't help there. So this means that in practice +we must refrain from delivering such a header upwards, regardless of any +outgoing protocol consideration. + +Applying such a lookup on all headers leaving the mux comes with a +significant performance hit, especially for large ones. A first attempt +was made at placing this into the HPACK decoder to refrain from learning +invalid literals but error reporting becomes more complicated. Additional +tests show that doing this within the HTX transcoding loop benefits from +the hot L1 cache, and that by skipping up to 8 bytes per iteration the +CPU cost remains within noise margin, around ~0.5%. + +This patch must be backported as far as 1.8 since this bug could be +exploited and serve as the base for an attack. In 2.0 and earlier the +fix must also be added to functions h2_make_h1_request() and +h2_make_h1_trailers() to handle legacy mode. It relies on previous patch +"MINOR: ist: add ist_find_ctl()" to speed up the control bytes lookup. + +All credits go to Tim for his detailed bug report and his initial patch. + +(cherry picked from commit 54f53ef7ce4102be596130b44c768d1818570344) +[wt: checks added to h2_make_h1_request() and h2_make_h1_trailers()] +Signed-off-by: Willy Tarreau +(cherry picked from commit c36c6789d1fc5e80e1f606a2828b72d5dd0ce22d) +[wt: minor ctx adjustments in trailers code -- was an H1 block in 1.9] +Signed-off-by: Willy Tarreau +(cherry picked from commit 00c8abd54e0d32cadca4183989e8274b1660d935) +[wt: dropped htx & trailers part since not present in 1.8] +Signed-off-by: Willy Tarreau +--- + src/h2.c | 25 +++++++++++++++++++++++++ + 1 file changed, 25 insertions(+) + +diff --git a/src/h2.c b/src/h2.c +index d7a0340..62b4241 100644 +--- a/src/h2.c ++++ b/src/h2.c +@@ -32,6 +32,23 @@ + #include + + ++/* Looks into for forbidden characters for header values (0x00, 0x0A, ++ * 0x0D), starting at pointer which must be within . Returns ++ * non-zero if such a character is found, 0 otherwise. When run on unlikely ++ * header match, it's recommended to first check for the presence of control ++ * chars using ist_find_ctl(). ++ */ ++static int has_forbidden_char(const struct ist ist, const char *start) ++{ ++ do { ++ if ((uint8_t)*start <= 0x0d && ++ (1U << (uint8_t)*start) & ((1<<13) | (1<<10) | (1<<0))) ++ return 1; ++ start++; ++ } while (start < ist.ptr + ist.len); ++ return 0; ++} ++ + /* Prepare the request line into <*ptr> (stopping at ) from pseudo headers + * stored in . indicates what was found so far. This should be + * called once at the detection of the first general header field or at the end +@@ -134,6 +151,7 @@ int h2_make_h1_request(struct http_hdr *list, char *out, int osize, unsigned int + { + struct ist phdr_val[H2_PHDR_NUM_ENTRIES]; + char *out_end = out + osize; ++ const char *ctl; + uint32_t fields; /* bit mask of H2_PHDR_FND_* */ + uint32_t idx; + int ck, lck; /* cookie index and last cookie index */ +@@ -158,6 +176,13 @@ int h2_make_h1_request(struct http_hdr *list, char *out, int osize, unsigned int + phdr = h2_str_to_phdr(list[idx].n); + } + ++ /* RFC7540#10.3: intermediaries forwarding to HTTP/1 must take care of ++ * rejecting NUL, CR and LF characters. ++ */ ++ ctl = ist_find_ctl(list[idx].v); ++ if (unlikely(ctl) && has_forbidden_char(list[idx].v, ctl)) ++ goto fail; ++ + if (phdr > 0 && phdr < H2_PHDR_NUM_ENTRIES) { + /* insert a pseudo header by its index (in phdr) and value (in value) */ + if (fields & ((1 << phdr) | H2_PHDR_FND_NONE)) { +-- +2.9.0 + diff -Nru haproxy-1.8.19/debian/patches/0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch haproxy-1.8.19/debian/patches/0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch --- haproxy-1.8.19/debian/patches/0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch 1970-01-01 00:00:00.000000000 +0000 +++ haproxy-1.8.19/debian/patches/0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch 2019-11-27 16:07:57.000000000 +0000 @@ -0,0 +1,79 @@ +From 4b37de078bfa850ea3d08d02e23b912fd5f8c168 Mon Sep 17 00:00:00 2001 +From: Willy Tarreau +Date: Sun, 24 Nov 2019 10:34:39 +0100 +Subject: BUG/MAJOR: h2: make header field name filtering stronger +MIME-Version: 1.0 +Content-Type: text/plain; charset=latin1 +Content-Transfer-Encoding: 8bit + +Tim Düsterhus found that the amount of sanitization we perform on HTTP +header field names received in H2 is insufficient. Currently we reject +upper case letters as mandated by RFC7540#8.1.2, but section 10.3 also +requires that intermediaries translating streams to HTTP/1 further +refine the filtering to also reject invalid names (which means any name +that doesn't match a token). There is a small trick here which is that +the colon character used to start pseudo-header names doesn't match a +token, so pseudo-header names fall into that category, thus we have to +swap the pseudo-header name lookup with this check so that we only check +from the second character (past the ':') in case of pseudo-header names. + +Another possibility could have been to perform this check only in the +HTX-to-H1 trancoder but doing would still expose the configured rules +and logs to such header names. + +This fix must be backported as far as 1.8 since this bug could be +exploited and serve as the base for an attack. In 2.0 and earlier, +functions h2_make_h1_request() and h2_make_h1_trailers() must also +be adapted to sanitize requests coming in legacy mode. + +(cherry picked from commit 146f53ae7e97dbfe496d0445c2802dd0a30b0878) +[wt: check added to h2_make_h1_request() and h2_make_h1_trailers()] +Signed-off-by: Willy Tarreau +(cherry picked from commit 5eaeec588d1de4f262f4ecffe0d39a36212965c3) +Signed-off-by: Willy Tarreau +(cherry picked from commit 8cf848ae60040620bd9293d912c3e3d68505379e) +[wt: dropped HTX and trailers parts since absent in 1.8; include h1.h] +Signed-off-by: Willy Tarreau +--- + src/h2.c | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +diff --git a/src/h2.c b/src/h2.c +index 62b4241..e5351d7 100644 +--- a/src/h2.c ++++ b/src/h2.c +@@ -30,7 +30,7 @@ + #include + #include + #include +- ++#include + + /* Looks into for forbidden characters for header values (0x00, 0x0A, + * 0x0D), starting at pointer which must be within . Returns +@@ -168,12 +168,17 @@ int h2_make_h1_request(struct http_hdr *list, char *out, int osize, unsigned int + } + else { + /* this can be any type of header */ +- /* RFC7540#8.1.2: upper case not allowed in header field names */ +- for (i = 0; i < list[idx].n.len; i++) +- if ((uint8_t)(list[idx].n.ptr[i] - 'A') < 'Z' - 'A') +- goto fail; +- ++ /* RFC7540#8.1.2: upper case not allowed in header field names. ++ * #10.3: header names must be valid (i.e. match a token). ++ * For pseudo-headers we check from 2nd char and for other ones ++ * from the first char, because HTTP_IS_TOKEN() also excludes ++ * the colon. ++ */ + phdr = h2_str_to_phdr(list[idx].n); ++ ++ for (i = !!phdr; i < list[idx].n.len; i++) ++ if ((uint8_t)(list[idx].n.ptr[i] - 'A') < 'Z' - 'A' || !HTTP_IS_TOKEN(list[idx].n.ptr[i])) ++ goto fail; + } + + /* RFC7540#10.3: intermediaries forwarding to HTTP/1 must take care of +-- +2.9.0 + diff -Nru haproxy-1.8.19/debian/patches/series haproxy-1.8.19/debian/patches/series --- haproxy-1.8.19/debian/patches/series 2019-01-14 18:57:09.000000000 +0000 +++ haproxy-1.8.19/debian/patches/series 2019-11-27 16:07:57.000000000 +0000 @@ -3,5 +3,10 @@ haproxy.service-add-documentation.patch haproxy.service-use-environment-variables.patch +# 20191125 security issue (no CVE) about HTTP/2 header validation +0001-MINOR-ist-add-ist_find_ctl.patch +0002-BUG-MAJOR-h2-reject-header-values-containing-invalid.patch +0003-BUG-MAJOR-h2-make-header-field-name-filtering-strong.patch + # applied during the build process: # debianize-dconv.patch