From 4b9b6044b228bac825f18fd9b54bd906c06b932e Mon Sep 17 00:00:00 2001 From: Willy Tarreau Date: Tue, 27 Oct 2026 16:29:23 +0100 Subject: [PATCH] MINOR: http/hardening: add "delete-unregistered-header-fields" Deletes from requests and responses any header field whose name starts with 'x-' (upper or lower case). These header fields are not registered with IANA and are not used across the internet by standard components. They may appear in untrusted requests in attacks trying to exploit some backend framework weaknesses however, as well as in old browsers that were trying to work around known server bugs (e.G. "X-Pad"). While it would be risky to block such requests, it is safe and even recommended to delete such headers from inbound internet requests. It is preferable not to do it on responses however when applications uses such header fields to report information to HAProxy. --- doc/configuration.txt | 12 ++++++++++++ include/haproxy/http-t.h | 1 + src/h1.c | 7 +++++++ src/h2.c | 16 ++++++++++++++++ src/h3.c | 10 ++++++++++ src/http.c | 1 + 6 files changed, 47 insertions(+) diff --git a/doc/configuration.txt b/doc/configuration.txt index 88384bb60..805818792 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -34546,6 +34546,18 @@ http-hardening [from ] recommended to enable on untrusted listeners. Note that this applies only after "reject-underscore-in-trailer-name" was accepted. + - delete-unregistered-header-fields (inbound, request and response) + Deletes from requests and responses any header field whose name starts + with 'x-' (upper or lower case). These header fields are not registered + with IANA and are not used across the internet by standard components. + They may appear in untrusted requests in attacks trying to exploit some + backend framework weaknesses however, as well as in old browsers that + were trying to work around known server bugs (e.G. "X-Pad"). While it + would be risky to block such requests, it is safe and even recommended + to delete such headers from inbound internet requests. It is preferable + not to do it on responses however when applications uses such header + fields to report information to HAProxy. + - 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 diff --git a/include/haproxy/http-t.h b/include/haproxy/http-t.h index 39b83c771..a88367aad 100644 --- a/include/haproxy/http-t.h +++ b/include/haproxy/http-t.h @@ -246,6 +246,7 @@ struct http_violation { #define HTTP_HRD_REJ_USCORE_TLR_NAM 0x00000008 #define HTTP_HRD_DEL_USCORE_HDR_NAM 0x00000010 #define HTTP_HRD_DEL_USCORE_TLR_NAM 0x00000020 +#define HTTP_HRD_DEL_UNREG_HDR 0x00000040 /* insert new masks above this line */ #define HTTP_HRD_ALL 0xFFFFFFFF diff --git a/src/h1.c b/src/h1.c index fa5b0c6bf..7fc979db4 100644 --- a/src/h1.c +++ b/src/h1.c @@ -1052,6 +1052,13 @@ int h1_headers_to_hdr_list(char *start, const char *stop, break; } + /* we may have to delete unregistered header names (X-) */ + if (unlikely(h1m->state != H1_MSG_TRAILERS && http_harden(HTTP_HRD_DEL_UNREG_HDR))) { + if (n.len > 1 && n.ptr[1] == '-' && + (n.ptr[0] == 'x' || n.ptr[0] == 'X')) + break; + } + if (isteqi(n, ist("transfer-encoding"))) { ret = h1_parse_xfer_enc_header(h1m, v); if (ret < 0) { diff --git a/src/h2.c b/src/h2.c index db6a689fd..cf911302f 100644 --- a/src/h2.c +++ b/src/h2.c @@ -405,6 +405,14 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms continue; } + /* we may have to delete unregistered header names (X-) */ + if (unlikely(http_harden(HTTP_HRD_DEL_UNREG_HDR))) { + if (list[idx].n.len > 1 && list[idx].n.ptr[1] == '-' && + (list[idx].n.ptr[0] == 'x' || list[idx].n.ptr[0] == 'X')) + /* skip it, so it won't be added */ + continue; + } + if (unlikely(!(fields & H2_PHDR_FND_NONE))) { /* no more pseudo-headers, time to build the request line */ sl = h2_prepare_htx_reqline(fields, phdr_val, htx, msgf); @@ -741,6 +749,14 @@ int h2_make_htx_response(struct http_hdr *list, struct htx *htx, unsigned int *m continue; } + /* we may have to delete unregistered header names (X-) */ + if (unlikely(http_harden(HTTP_HRD_DEL_UNREG_HDR))) { + if (list[idx].n.len > 1 && list[idx].n.ptr[1] == '-' && + (list[idx].n.ptr[0] == 'x' || list[idx].n.ptr[0] == 'X')) + /* skip it, so it won't be added */ + continue; + } + if (!(fields & H2_PHDR_FND_NONE)) { /* no more pseudo-headers, time to build the status line */ sl = h2_prepare_htx_stsline(fields, phdr_val, htx, msgf); diff --git a/src/h3.c b/src/h3.c index ee3598af7..226d1a8b4 100644 --- a/src/h3.c +++ b/src/h3.c @@ -1071,6 +1071,16 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, continue; } + /* we may have to delete unregistered header names (X-) */ + if (unlikely(http_harden(HTTP_HRD_DEL_UNREG_HDR))) { + if (list[hdr_idx].n.len > 1 && list[hdr_idx].n.ptr[1] == '-' && + (list[hdr_idx].n.ptr[0] == 'x' || list[hdr_idx].n.ptr[0] == 'X')) { + /* skip it, so it won't be added */ + ++hdr_idx; + continue; + } + } + if (isteq(list[hdr_idx].n, ist("host"))) { struct ist prev_auth = authority; diff --git a/src/http.c b/src/http.c index 18f0eb9a7..94a19a036 100644 --- a/src/http.c +++ b/src/http.c @@ -1781,6 +1781,7 @@ const struct http_hardening_kw http_hardening_kws[] = { { "reject-underscore-in-trailer-name", HTTP_HRD_REJ_USCORE_TLR_NAM, HTTP_DIR_BOTH }, { "delete-underscore-in-header-name", HTTP_HRD_DEL_USCORE_HDR_NAM, HTTP_DIR_BOTH }, { "delete-underscore-in-trailer-name", HTTP_HRD_DEL_USCORE_TLR_NAM, HTTP_DIR_BOTH }, + { "delete-unregistered-header-fields",HTTP_HRD_DEL_UNREG_HDR, HTTP_DIR_BOTH }, /* insert new hardening options above this line */ { "all", HTTP_HRD_ALL, HTTP_DIR_BOTH }, { NULL, 0 } -- 2.47.3