Commit message: BUG/MAJOR: htx: fix missing header name length check in htx_add_header/trailer

Ori Hollander of JFrog Security reported that htx_add_header() and
htx_add_trailer() were missing a length check on the header name. While
this does not allow to overwrite any memory area, it results in bits of
the header name length to slip into the header value length and may
result in forging certain header names on the input. The sad thing here
is that a FIXME comment was present suggesting to add the required length
checks :-(

The injected headers are visible to the HTTP internals and to the config
rules, so haproxy will generally stay synchronized with the server. But
there is one exception which is the content-length header field, because
it is already deduplicated on the input, but before being indexed. As
such, injecting a content-length header after the deduplication stage
may be abused to present a different, shorter one on the other side and
help build a request smuggling attack, or even maybe a response splitting
attack. CVE-2021-40346 was assigned to this problem.

As a mitigation measure, it is sufficient to verify that no more than
one such header is present in any message, which is normally the case
thanks to the duplicate checks:

   http-request  deny if { req.hdr_cnt(content-length) gt 1 }
   http-response deny if { res.hdr_cnt(content-length) gt 1 }

This must be backported to all HTX-enabled versions, hence as far as 2.0.
In 2.3 and earlier, the functions are in src/htx.c instead.

Many thanks to Ori for his work and his responsible report!
CVE: CVE-2021-40346
CWE: CWE-190
Vulnerability description: An integer overflow exists in HAProxy 2.0 through 2.5 in htx_add_header that can be exploited to perform an HTTP request smuggling attack, allowing an attacker to bypass all configured http-request HAProxy ACLs and possibly other ACLs.

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_tail_blk(const struct htx *htx)
{
	int32_t tail = htx_get_tail(htx);

	return ((tail == -1) ? NULL : htx_get_blk(htx, tail));
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_prev_blk(const struct htx *htx,
					       const struct htx_blk *blk)
{
	int32_t pos;

	pos = htx_get_prev(htx, htx_get_blk_pos(htx, blk));
	return ((pos == -1) ? NULL : htx_get_blk(htx, pos));
}

Project: haproxy
File: htx.h

static inline int htx_is_empty(const struct htx *htx)
{
	return (htx->head == -1);
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_req_uri(const struct htx_sl *sl)
{
	return htx_sl_p2(sl);
}

Project: haproxy
File: htx.h

static inline struct htx *htxbuf(const struct buffer *buf)
{
	struct htx *htx;

	if (b_is_null(buf))
		return &htx_empty;
	htx = ((struct htx *)(buf->area));
	if (!b_data(buf)) {
		htx->size = buf->size - sizeof(*htx);
		htx_reset(htx);
	}
	return htx;
}

Project: haproxy
File: htx.h

static inline uint32_t htx_free_space(const struct htx *htx)
{
	return (htx->size - htx_used_space(htx));
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_res_vsn(const struct htx_sl *sl)
{
	return htx_sl_p1(sl);
}

Project: haproxy
File: htx.h

static inline uint32_t htx_free_data_space(const struct htx *htx)
{
	uint32_t free = htx_free_space(htx);

	if (free < sizeof(struct htx_blk))
		return 0;
	return (free - sizeof(struct htx_blk));
}

Project: haproxy
File: htx.h

static inline uint32_t htx_meta_space(const struct htx *htx)
{
	if (htx->tail == -1)
		return 0;

	return ((htx->tail + 1 - htx->head) * sizeof(struct htx_blk));
}

Project: haproxy
File: htx.h

static inline int htx_nbblks(const struct htx *htx)
{
	return ((htx->head != -1) ? (htx->tail + 1 - htx->head) : 0);
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_p2(const struct htx_sl *sl)
{
	return ist2(HTX_SL_P2_PTR(sl), HTX_SL_P2_LEN(sl));
}

Project: haproxy
File: htx.h

static inline uint32_t htx_used_space(const struct htx *htx)
{
	return (htx->data + htx_meta_space(htx));
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_add_trailer(struct htx *htx, const struct ist name,
					      const struct ist value)
{
	struct htx_blk *blk;

	if (name.len > 255 || value.len > 1048575)
		return NULL;

	blk = htx_add_blk(htx, HTX_BLK_TLR, name.len + value.len);
	if (!blk)
		return NULL;

	blk->info += (value.len << 8) + name.len;
	ist2bin_lc(htx_get_blk_ptr(htx, blk), name);
	memcpy(htx_get_blk_ptr(htx, blk)  + name.len, value.ptr, value.len);
	return blk;
}

Project: haproxy
File: htx.h

static inline const char *htx_blk_type_str(enum htx_blk_type type)
{
	switch (type) {
		case HTX_BLK_REQ_SL: return "HTX_BLK_REQ_SL";
		case HTX_BLK_RES_SL: return "HTX_BLK_RES_SL";
		case HTX_BLK_HDR:    return "HTX_BLK_HDR";
		case HTX_BLK_EOH:    return "HTX_BLK_EOH";
		case HTX_BLK_DATA:   return "HTX_BLK_DATA";
		case HTX_BLK_TLR:    return "HTX_BLK_TLR";
		case HTX_BLK_EOT:    return "HTX_BLK_EOT";
		case HTX_BLK_UNUSED: return "HTX_BLK_UNUSED";
		default:             return "HTX_BLK_???";
	};
}

Project: haproxy
File: htx.h

static inline void htx_dump(struct buffer *chunk, const struct htx *htx, int full)
{
	int32_t pos;

	chunk_appendf(chunk, " htx=%p(size=%u,data=%u,used=%u,wrap=%s,flags=0x%08x,extra=%llu,"
		      "first=%d,head=%d,tail=%d,tail_addr=%d,head_addr=%d,end_addr=%d)",
		      htx, htx->size, htx->data, htx_nbblks(htx), (!htx->head_addr) ? "NO" : "YES",
		      htx->flags, (unsigned long long)htx->extra, htx->first, htx->head, htx->tail,
		      htx->tail_addr, htx->head_addr, htx->end_addr);

	if (!full || !htx_nbblks(htx))
		return;
	chunk_memcat(chunk, "\n", 1);

	for (pos = htx_get_head(htx); pos != -1; pos = htx_get_next(htx, pos)) {
		struct htx_sl     *sl;
		struct htx_blk    *blk  = htx_get_blk(htx, pos);
		enum htx_blk_type  type = htx_get_blk_type(blk);
		uint32_t           sz   = htx_get_blksz(blk);
		struct ist         n, v;

		n = htx_get_blk_name(htx, blk);
		v = htx_get_blk_value(htx, blk);

		if (type == HTX_BLK_REQ_SL || type == HTX_BLK_RES_SL) {
			sl = htx_get_blk_ptr(htx, blk);
			chunk_appendf(chunk, "\t\t[%u] type=%-17s - size=%-6u - addr=%-6u\t%.*s %.*s %.*s\n",
				      pos, htx_blk_type_str(type), sz, blk->addr,
				      HTX_SL_P1_LEN(sl), HTX_SL_P1_PTR(sl),
				      HTX_SL_P2_LEN(sl), HTX_SL_P2_PTR(sl),
				      HTX_SL_P3_LEN(sl), HTX_SL_P3_PTR(sl));
		}
		else if (type == HTX_BLK_HDR || type == HTX_BLK_TLR)
			chunk_appendf(chunk, "\t\t[%u] type=%-17s - size=%-6u - addr=%-6u\t%.*s: %.*s\n",
				      pos, htx_blk_type_str(type), sz, blk->addr,
				      (int)MIN(n.len, 32), n.ptr,
				      (int)MIN(v.len, 64), v.ptr);
		else
			chunk_appendf(chunk, "\t\t[%u] type=%-17s - size=%-6u - addr=%-6u%s\n",
				      pos, htx_blk_type_str(type), sz, blk->addr,
				      (!v.len ? "\t<empty>" : ""));
	}
}

Project: haproxy
File: htx.h

static inline void htx_skip_msg_payload(struct htx *htx)
{
	struct htx_blk *blk = htx_get_first_blk(htx);

	while (blk) {
		enum htx_blk_type type = htx_get_blk_type(blk);

		blk = ((type > HTX_BLK_EOH)
		       ? htx_remove_blk(htx, blk)
		       : htx_get_next_blk(htx, blk));
	}
}

Project: haproxy
File: htx.h

static inline int32_t htx_get_head(const struct htx *htx)
{
	return htx->head;
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_p1(const struct htx_sl *sl)
{
	return ist2(HTX_SL_P1_PTR(sl), HTX_SL_P1_LEN(sl));
}

Project: haproxy
File: htx.h

static inline int32_t htx_get_prev(const struct htx *htx, uint32_t pos)
{
	if (htx->head == -1 || pos == htx->head)
		return -1;
	return (pos - 1);
}

Project: haproxy
File: htx.h

static inline int htx_is_not_empty(const struct htx *htx)
{
	return (htx->head != -1);
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_p3(const struct htx_sl *sl)
{
	return ist2(HTX_SL_P3_PTR(sl), HTX_SL_P3_LEN(sl));
}

Project: haproxy
File: htx.h

static inline int htx_almost_full(const struct htx *htx)
{
	if (!htx->size || htx_free_space(htx) < htx->size / 4)
		return 1;
	return 0;
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_req_meth(const struct htx_sl *sl)
{
	return htx_sl_p1(sl);
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_blk(const struct htx *htx, uint32_t pos)
{
	return (struct htx_blk *)(htx->blocks + htx_pos_to_addr(htx, pos));
}

Project: haproxy
File: htx.h

static inline void *htx_get_blk_ptr(const struct htx *htx, const struct htx_blk *blk)
{
	return ((void *)htx->blocks + blk->addr);
}

Project: haproxy
File: htx.h

static inline void htx_change_blk_value_len(struct htx *htx, struct htx_blk *blk, uint32_t newlen)
{
	enum htx_blk_type type = htx_get_blk_type(blk);
	uint32_t oldlen, sz;
	int32_t delta;

	sz = htx_get_blksz(blk);
	switch (type) {
		case HTX_BLK_HDR:
		case HTX_BLK_TLR:
			oldlen = (blk->info >> 8) & 0xfffff;
			blk->info = (type << 28) + (newlen << 8) + (blk->info & 0xff);
			break;
		default:
			oldlen = blk->info & 0xfffffff;
			blk->info = (type << 28) + newlen;
			break;
	}

	/* Update HTTP message */
	delta = (newlen - oldlen);
	htx->data += delta;
	if (blk->addr+sz == htx->tail_addr)
		htx->tail_addr += delta;
	else if (blk->addr+sz == htx->head_addr)
		htx->head_addr += delta;
}

Project: haproxy
File: htx.h

static inline void htx_set_blk_value_len(struct htx_blk *blk, uint32_t vlen)
{
	enum htx_blk_type type = htx_get_blk_type(blk);

	switch (type) {
		case HTX_BLK_HDR:
		case HTX_BLK_TLR:
			blk->info = (type << 28) + (vlen << 8) + (blk->info & 0xff);
			break;
		case HTX_BLK_REQ_SL:
		case HTX_BLK_RES_SL:
		case HTX_BLK_DATA:
			blk->info = (type << 28) + vlen;
			break;
		default:
			/* Unexpected case */
			break;
	}
}

Project: haproxy
File: htx.h

static inline uint32_t htx_get_max_blksz(const struct htx *htx, int32_t max)
{
	uint32_t free = htx_free_space(htx);

	if (max != -1 && free > max)
		free = max;
	if (free < sizeof(struct htx_blk))
		return 0;
	return (free - sizeof(struct htx_blk));
}

Project: haproxy
File: htx.h

static inline enum htx_blk_type htx_get_blk_type(const struct htx_blk *blk)
{
	return (blk->info >> 28);
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_res_code(const struct htx_sl *sl)
{
	return htx_sl_p2(sl);
}

Project: haproxy
File: htx.h

static inline struct ist htx_get_blk_value(const struct htx *htx, const struct htx_blk *blk)
{
	enum htx_blk_type type = htx_get_blk_type(blk);
	struct ist ret;

	switch (type) {
		case HTX_BLK_HDR:
		case HTX_BLK_TLR:
			ret.ptr = htx_get_blk_ptr(htx, blk) + (blk->info & 0xff);
			ret.len = (blk->info >> 8) & 0xfffff;
			break;

		case HTX_BLK_REQ_SL:
		case HTX_BLK_RES_SL:
		case HTX_BLK_DATA:
			ret.ptr = htx_get_blk_ptr(htx, blk);
			ret.len = blk->info & 0xfffffff;
			break;

		default:
			return ist("");
	}
	return ret;
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_add_header(struct htx *htx, const struct ist name,
					     const struct ist value)
{
	struct htx_blk *blk;

	if (name.len > 255 || value.len > 1048575)
		return NULL;

	blk = htx_add_blk(htx, HTX_BLK_HDR, name.len + value.len);
	if (!blk)
		return NULL;

	blk->info += (value.len << 8) + name.len;
	ist2bin_lc(htx_get_blk_ptr(htx, blk), name);
	memcpy(htx_get_blk_ptr(htx, blk)  + name.len, value.ptr, value.len);
	return blk;
}

Project: haproxy
File: htx.h

static inline uint32_t htx_pos_to_addr(const struct htx *htx, uint32_t pos)
{
	return htx->size - (pos + 1) * sizeof(struct htx_blk);
}

Project: haproxy
File: htx.h

static inline enum htx_blk_type htx_get_head_type(const struct htx *htx)
{
	struct htx_blk *blk = htx_get_head_blk(htx);

	return (blk ? htx_get_blk_type(blk) : HTX_BLK_UNUSED);
}

Project: haproxy
File: htx.h

static inline uint32_t htx_get_blk_pos(const struct htx *htx, const struct htx_blk *blk)
{
	return ((htx->blocks + htx->size - (char *)blk) / sizeof(struct htx_blk) - 1);
}

Project: haproxy
File: htx.h

static inline enum htx_blk_type htx_get_first_type(const struct htx *htx)
{
	struct htx_blk *blk = htx_get_first_blk(htx);

	return (blk ? htx_get_blk_type(blk) : HTX_BLK_UNUSED);
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_first_blk(const struct htx *htx)
{
	int32_t pos;

	pos = htx_get_first(htx);
	return ((pos == -1) ? NULL : htx_get_blk(htx, pos));
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_req_vsn(const struct htx_sl *sl)
{
	return htx_sl_p3(sl);
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_add_all_headers(struct htx *htx, const struct http_hdr *hdrs)
{
	int i;

	for (i = 0; hdrs[i].n.len; i++) {
		if (!htx_add_header(htx, hdrs[i].n, hdrs[i].v))
			return NULL;
	}
	return htx_add_endof(htx, HTX_BLK_EOH);
}

Project: haproxy
File: htx.h

static inline struct htx *htx_from_buf(struct buffer *buf)
{
	struct htx *htx = htxbuf(buf);

	b_set_data(buf, b_size(buf));
	return htx;
}

Project: haproxy
File: htx.h

static inline int htx_expect_more(const struct htx *htx)
{
	return !(htx->flags & HTX_FL_EOM);
}

Project: haproxy
File: htx.h

static inline void htx_to_buf(struct htx *htx, struct buffer *buf)
{
	if ((htx->head == -1) &&
	    !(htx->flags & (HTX_FL_PARSING_ERROR|HTX_FL_PROCESSING_ERROR))) {
		htx_reset(htx);
		b_set_data(buf, 0);
	}
	else
		b_set_data(buf, b_size(buf));
}

Project: haproxy
File: htx.h

static inline int32_t htx_get_next(const struct htx *htx, uint32_t pos)
{
	if (htx->tail == -1 || pos == htx->tail)
		return -1;
	return (pos + 1);

}

Project: haproxy
File: htx.h

static inline enum htx_blk_type htx_get_tail_type(const struct htx *htx)
{
	struct htx_blk *blk = htx_get_tail_blk(htx);

	return (blk ? htx_get_blk_type(blk) : HTX_BLK_UNUSED);
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_next_blk(const struct htx *htx,
					       const struct htx_blk *blk)
{
	int32_t pos;

	pos = htx_get_next(htx, htx_get_blk_pos(htx, blk));
	return ((pos == -1) ? NULL : htx_get_blk(htx, pos));
}

Project: haproxy
File: htx.h

static inline struct ist htx_sl_res_reason(const struct htx_sl *sl)
{
	return htx_sl_p3(sl);
}

Project: haproxy
File: htx.h

static inline int32_t htx_get_first(const struct htx *htx)
{
	return htx->first;
}

Project: haproxy
File: htx.h

static inline uint32_t htx_get_blksz(const struct htx_blk *blk)
{
	enum htx_blk_type type = htx_get_blk_type(blk);

	switch (type) {
		case HTX_BLK_HDR:
		case HTX_BLK_TLR:
			/*       name.length       +        value.length        */
			return ((blk->info & 0xff) + ((blk->info >> 8) & 0xfffff));
		default:
			/*         value.length      */
			return (blk->info & 0xfffffff);
	}
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_add_endof(struct htx *htx, enum htx_blk_type type)
{
	struct htx_blk *blk;

	blk = htx_add_blk(htx, type, 1);
	if (!blk)
		return NULL;

	blk->info += 1;
	return blk;
}

Project: haproxy
File: htx.h

static inline int htx_is_unique_blk(const struct htx *htx,
				    const struct htx_blk *blk)
{
	return (htx_get_blksz(blk) == htx->data);
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_add_all_trailers(struct htx *htx, const struct http_hdr *hdrs)
{
	int i;

	for (i = 0; hdrs[i].n.len; i++) {
		if (!htx_add_trailer(htx, hdrs[i].n, hdrs[i].v))
			return NULL;
	}
	return htx_add_endof(htx, HTX_BLK_EOT);
}

Project: haproxy
File: htx.h

static inline struct htx_sl *htx_add_stline(struct htx *htx, enum htx_blk_type type, unsigned int flags,
					    const struct ist p1, const struct ist p2, const struct ist p3)
{
	struct htx_blk *blk;
	struct htx_sl  *sl;
	uint32_t size;

	if (type != HTX_BLK_REQ_SL && type != HTX_BLK_RES_SL)
		return NULL;

	size = sizeof(*sl) + p1.len + p2.len + p3.len;

	blk = htx_add_blk(htx, type, size);
	if (!blk)
		return NULL;
	blk->info += size;

	sl = htx_get_blk_ptr(htx, blk);
	sl->flags = flags;

	HTX_SL_P1_LEN(sl) = p1.len;
	HTX_SL_P2_LEN(sl) = p2.len;
	HTX_SL_P3_LEN(sl) = p3.len;

	memcpy(HTX_SL_P1_PTR(sl), p1.ptr, p1.len);
	memcpy(HTX_SL_P2_PTR(sl), p2.ptr, p2.len);
	memcpy(HTX_SL_P3_PTR(sl), p3.ptr, p3.len);

	return sl;
}

Project: haproxy
File: htx.h

static inline void htx_cut_data_blk(struct htx *htx, struct htx_blk *blk, uint32_t n)
{
	if (blk->addr == htx->end_addr)
		htx->end_addr += n;
	blk->addr += n;
	blk->info -= n;
	htx->data -= n;
}

Project: haproxy
File: htx.h

static inline int32_t htx_get_tail(const struct htx *htx)
{
	return htx->tail;
}

Project: haproxy
File: htx.h

static inline size_t buf_room_for_htx_data(const struct buffer *buf)
{
	size_t room;

	room = b_room(buf);
	if (room <= sizeof(struct htx) + 2 * sizeof(struct htx_blk))
		room = 0;
	else
		room -= sizeof(struct htx) + 2 * sizeof(struct htx_blk);

	return room;
}

Project: haproxy
File: htx.h

static inline struct ist htx_get_blk_name(const struct htx *htx, const struct htx_blk *blk)
{
	enum htx_blk_type type = htx_get_blk_type(blk);
	struct ist ret;

	switch (type) {
		case HTX_BLK_HDR:
		case HTX_BLK_TLR:
			ret.ptr = htx_get_blk_ptr(htx, blk);
			ret.len = blk->info & 0xff;
			break;

		default:
			return ist("");
	}
	return ret;
}

Project: haproxy
File: htx.h

static inline struct htx_blk *htx_get_head_blk(const struct htx *htx)
{
	int32_t head = htx_get_head(htx);

	return ((head == -1) ? NULL : htx_get_blk(htx, head));
}

Project: haproxy
File: htx.h

static inline int htx_copy_msg(struct htx *htx, const struct buffer *msg)
{
	/* The destination HTX message is empty, we can do a raw copy */
	if (htx_is_empty(htx)) {
		memcpy(htx, msg->area, msg->size);
		return 1;
	}

	/* Otherwise, we need to append the HTX message */
	return htx_append_msg(htx, htxbuf(msg));
}

Project: haproxy
File: htx.h

static inline void htx_reset(struct htx *htx)
{
	htx->tail = htx->head  = htx->first = -1;
	htx->data = 0;
	htx->tail_addr = htx->head_addr = htx->end_addr = 0;
	htx->extra = 0;
	htx->flags = HTX_FL_NONE;
}