]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http/hardening: implement hardening "reject-duplicate-host-header"
authorWilly Tarreau <w@1wt.eu>
Mon, 26 Oct 2026 18:53:06 +0000 (19:53 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 21:45:32 +0000 (22:45 +0100)
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.

doc/configuration.txt
include/haproxy/http-t.h
src/h1.c
src/h2.c
src/h3.c
src/http.c

index e08b23d67dc116bb34778a51996fc1c4a3a68c37..0a2fc19ae450e19bafe9e589ef099dfc5cc628df 100644 (file)
@@ -34525,6 +34525,11 @@ http-hardening <name> [from <name2>]
         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:
index 7635231fb23b86bb81ba16b74c771e621fb1d7a8..5fb443c02bc3a08ac79f390179b011119c2005c7 100644 (file)
@@ -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
 
index 8a1d455d7c6aa32d3434cde1f9e493e03048bdd4..3d3e40068dbfb313a239645d750ed12e5b691972 100644 (file)
--- 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;
                                        }
                                }
index bad6f57e931aca130b4ee2cc195b2dbdc26873ee..d67f363fcfdd6e2e621d859efd34ab6354a9b707 100644 (file)
--- 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))
index 4f99216370e7b3a027ba82eebaf3e37bae54c448..6958977fe19aa2c134bdb592d05ee6db74163f81 100644 (file)
--- 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;
                        }
index 8cf380a358628d6da314282a95addfcef3c31fef..3113c43273a703b93ceb0583812c948b89aa42d2 100644 (file)
@@ -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 }