From: Willy Tarreau Date: Mon, 26 Oct 2026 18:53:06 +0000 (+0100) Subject: MINOR: http/hardening: implement hardening "reject-duplicate-host-header" X-Git-Tag: v3.5-dev11~22 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=9d6b4467a574f0fe605f0e8c06704b109f7776e2;p=haproxy.git MINOR: http/hardening: implement hardening "reject-duplicate-host-header" Rejects requests containing more than one "Host" header field, even if valid and identical. This is recommended on untrusted listeners. Note that even if not rejected, duplicate host header fields are folded into a single one. This was done for h1/h2/h3 which all had the explicit check to perform deduplication. In h2 and h3 we also had to make sure not to count host+authority as a duplicate, so that requires a host counter as a local var, but that's cheap. Note that h3 currently doesn't check that duplicate headers are strictly equal, only the first one is kept. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index e08b23d67..0a2fc19ae 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -34525,6 +34525,11 @@ http-hardening [from ] This keyword covers all known keywords together. It is essentially used to enable or disable all checks at once. + - reject-duplicate-host-header (inbound request) + Rejects requests containing more than one "Host" header field, even if + valid and identical. This is recommended on untrusted listeners. Note that + even if not rejected, duplicate host header fields are folded into a + single one. /* * Local variables: diff --git a/include/haproxy/http-t.h b/include/haproxy/http-t.h index 7635231fb..5fb443c02 100644 --- a/include/haproxy/http-t.h +++ b/include/haproxy/http-t.h @@ -240,6 +240,7 @@ struct http_violation { */ #define HTTP_HRD_NONE 0x00000000 /* insert new masks below this line */ +#define HTTP_HRD_REJ_DUP_HOST_HDR 0x00000001 /* insert new masks above this line */ #define HTTP_HRD_ALL 0xFFFFFFFF diff --git a/src/h1.c b/src/h1.c index 8a1d455d7..3d3e40068 100644 --- a/src/h1.c +++ b/src/h1.c @@ -1089,7 +1089,12 @@ int h1_headers_to_hdr_list(char *start, const char *stop, ptr = v.ptr; /* Set ptr on the error */ goto http_msg_invalid; } - /* if the same host, skip it */ + /* if the same host, skip or reject it */ + if (http_harden(HTTP_HRD_REJ_DUP_HOST_HDR)) { + state = H1_MSG_HDR_L2_LWS; + ptr = v.ptr; /* Set ptr on the error */ + goto http_msg_invalid; + } break; } } diff --git a/src/h2.c b/src/h2.c index bad6f57e9..d67f363fc 100644 --- a/src/h2.c +++ b/src/h2.c @@ -325,6 +325,7 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms uint32_t data_ofs = htx->data; /* used to rollback on error */ struct ist phdr_val[H2_PHDR_NUM_ENTRIES]; uint32_t fields; /* bit mask of H2_PHDR_FND_* */ + int host_cnt = 0; uint32_t idx; int ck, lck; /* cookie index and last cookie index */ int phdr; @@ -416,9 +417,14 @@ 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) + host_cnt++; + /* skip duplicates, including real host from :authority */ + if (fields & H2_PHDR_FND_HOST) { + /* check hardening, don't count :authority as a dup */ + if (host_cnt > 1 && http_harden(HTTP_HRD_REJ_DUP_HOST_HDR)) + goto fail; continue; + } fields |= H2_PHDR_FND_HOST; if (http_authority_has_forbidden_char(list[idx].v)) diff --git a/src/h3.c b/src/h3.c index 4f9921637..6958977fe 100644 --- a/src/h3.c +++ b/src/h3.c @@ -720,6 +720,7 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, struct ist meth = IST_NULL, path = IST_NULL; struct ist scheme = IST_NULL, authority = IST_NULL; struct ist uri; + int host_cnt = 0; int hdr_idx, ret; int cookie = -1, last_cookie = -1, i; int relaxed = !!(h3c->qcc->proxy->options2 & PR_O2_REQBUG_OK); @@ -1058,6 +1059,7 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, if (isteq(list[hdr_idx].n, ist("host"))) { struct ist prev_auth = authority; + host_cnt++; 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; @@ -1068,6 +1070,15 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, if (isttest(prev_auth)) { /* skip duplicate Host header */ + /* TODO: need to check if it matches the previous one */ + /* check hardening, but don't count :authority as a dup */ + if (host_cnt > 1 && http_harden(HTTP_HRD_REJ_DUP_HOST_HDR)) { + h3s->err = H3_ERR_MESSAGE_ERROR; + qcc_report_glitch(h3c->qcc, 1); + len = -1; + goto out; + } + ++hdr_idx; continue; } diff --git a/src/http.c b/src/http.c index 8cf380a35..3113c4327 100644 --- a/src/http.c +++ b/src/http.c @@ -1775,6 +1775,7 @@ static int http_parse_acceptable_http_violations(char **args, int section_type, /* list of known HTTP hardening options */ const struct http_hardening_kw http_hardening_kws[] = { /* insert new hardening options below this line */ + { "reject-duplicate-host-header", HTTP_HRD_REJ_DUP_HOST_HDR, HTTP_DIR_REQ }, /* insert new hardening options above this line */ { "all", HTTP_HRD_ALL, HTTP_DIR_BOTH }, { NULL, 0 }