]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http/harderning: implement "delete-underscore-in-header-name"
authorWilly Tarreau <w@1wt.eu>
Tue, 27 Oct 2026 14:53:06 +0000 (15:53 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 21:45:32 +0000 (22:45 +0100)
Deletes from requests and responses any header field whose name
contains an underscore character ('_' or \x5C). While this character is
perfectly permitted, some application frameworks which create automatic
variables already turn the dash ('-') into an underscore in variable
names, leading to an ambiguity with the underscore which can result in
the same variable name. This could be abused to bypass some
access-control policy for example. No standard HTTP header uses this,
so it is recommended to enable on untrusted listeners. Note that this
applies only after "reject-underscore-in-header-name" was accepted.

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

index ab698debb32726bdd68167cb90633bc90d31cb18..ef5800df4b7d0de89703091b615df77dbde8a00c 100644 (file)
@@ -34525,6 +34525,17 @@ http-hardening <name> [from <name2>]
         This keyword covers all known keywords together. It is essentially used
         to enable or disable all checks at once.
 
+  - delete-underscore-in-header-name (inbound, request and response)
+        Deletes from requests and responses any header field whose name
+        contains an underscore character ('_' or \x5C). While this character is
+        perfectly permitted, some application frameworks which create automatic
+        variables already turn the dash ('-') into an underscore in variable
+        names, leading to an ambiguity with the underscore which can result in
+        the same variable name. This could be abused to bypass some
+        access-control policy for example. No standard HTTP header uses this,
+        so it is recommended to enable on untrusted listeners. Note that this
+        applies only after "reject-underscore-in-header-name" was accepted.
+
   - 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
@@ -34546,7 +34557,8 @@ http-hardening <name> [from <name2>]
         variable names, leading to an ambiguity with the underscore which can
         result in the same variable name. This could be abused to bypass some
         access-control policy for example. No standard HTTP header uses this,
-        so it is safe to enable on untrusted listeners.
+        so it is safe to enable on untrusted listeners. See also the
+        "delete-underscore-in-header-name" variant.
 
   - reject-underscore-in-trailer-name (inbound, request and response)
         Rejects requests and responses in which one tailer field name contains
index efe0bd3401a01acb6b5e78ee54e3e8857de52b07..baf99e9eaae780c7116d6b5d1c8d851b7e3b8f29 100644 (file)
@@ -244,6 +244,7 @@ struct http_violation {
 #define HTTP_HRD_REJ_LCASE_METHOD           0x00000002
 #define HTTP_HRD_REJ_USCORE_HDR_NAM         0x00000004
 #define HTTP_HRD_REJ_USCORE_TLR_NAM         0x00000008
+#define HTTP_HRD_DEL_USCORE_HDR_NAM         0x00000010
 /* insert new masks above this line */
 #define HTTP_HRD_ALL                        0xFFFFFFFF
 
index c6c9fccbfbfa6683d1142063a1a755b9d0b12610..459e4cd20e491e0fcbaf0470305e73b2a73b9b28 100644 (file)
--- a/src/h1.c
+++ b/src/h1.c
@@ -1038,6 +1038,18 @@ int h1_headers_to_hdr_list(char *start, const char *stop,
                                                break;
                                }
 
+                               /* we may have to delete headers with underscores
+                                * in their names. These ones are already marked
+                                * as found when not rejected so we can avoid a
+                                * costly lookup in normal times.
+                                */
+                               if (unlikely(http_hdng_chk.found & HTTP_HRD_REJ_USCORE_HDR_NAM) &&
+                                   ((h1m->state != H1_MSG_TRAILERS && istlen(istfind(n, '_')) &&
+                                     http_harden(HTTP_HRD_DEL_USCORE_HDR_NAM)))) {
+                                       /* skip it, so it won't be added */
+                                       break;
+                               }
+
                                if (isteqi(n, ist("transfer-encoding"))) {
                                        ret = h1_parse_xfer_enc_header(h1m, v);
                                        if (ret < 0) {
index fa9486bdb2a04afbc81e8b441b0248acfda64735..db6a689fd4efd0a0b7e4bdd24c7908f7e7dbce69 100644 (file)
--- a/src/h2.c
+++ b/src/h2.c
@@ -394,6 +394,17 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms
                }
 
                /* regular header field in (name,value) */
+
+               /* we may have to delete headers with underscores in their
+                * names. These ones are already marked as found when not
+                * rejected so we can avoid a costly lookup in normal times.
+                */
+               if (unlikely(http_hdng_chk.found & HTTP_HRD_REJ_USCORE_HDR_NAM) &&
+                   istlen(istfind(list[idx].n, '_')) && http_harden(HTTP_HRD_DEL_USCORE_HDR_NAM)) {
+                       /* 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);
@@ -719,6 +730,17 @@ int h2_make_htx_response(struct http_hdr *list, struct htx *htx, unsigned int *m
                }
 
                /* regular header field in (name,value) */
+
+               /* we may have to delete headers with underscores in their
+                * names. These ones are already marked as found when not
+                * rejected so we can avoid a costly lookup in normal times.
+                */
+               if (unlikely(http_hdng_chk.found & HTTP_HRD_REJ_USCORE_HDR_NAM) &&
+                   istlen(istfind(list[idx].n, '_')) && http_harden(HTTP_HRD_DEL_USCORE_HDR_NAM)) {
+                       /* 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);
index 3fbfa2758c94a8aa2ea040b9f3d62abe5691b077..ee3598af76faf17db0b07c37be28da5b6aa98d52 100644 (file)
--- a/src/h3.c
+++ b/src/h3.c
@@ -1060,6 +1060,17 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
                        goto out;
                }
 
+               /* we may have to delete headers with underscores in their
+                * names. These ones are already marked as found when not
+                * rejected so we can avoid a costly lookup in normal times.
+                */
+               if (unlikely(http_hdng_chk.found & HTTP_HRD_REJ_USCORE_HDR_NAM) &&
+                   istlen(istfind(list[hdr_idx].n, '_')) && http_harden(HTTP_HRD_DEL_USCORE_HDR_NAM)) {
+                       /* skip it, so it won't be added */
+                       ++hdr_idx;
+                       continue;
+               }
+
                if (isteq(list[hdr_idx].n, ist("host"))) {
                        struct ist prev_auth = authority;
 
index 0ef5376674dce56b17e388c826269e0561ffa287..341dee01f8337bc5c096d2ddbf30a8acee5effb8 100644 (file)
@@ -1779,6 +1779,7 @@ const struct http_hardening_kw http_hardening_kws[] = {
        { "reject-lowercase-method",        HTTP_HRD_REJ_LCASE_METHOD, HTTP_DIR_REQ  },
        { "reject-underscore-in-header-name", HTTP_HRD_REJ_USCORE_HDR_NAM, HTTP_DIR_BOTH },
        { "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 },
        /* insert new hardening options above this line */
        { "all",                            HTTP_HRD_ALL,              HTTP_DIR_BOTH },
        { NULL, 0 }