]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http/hardening: implement hardening "reject-underscore-in-header-name"
authorWilly Tarreau <w@1wt.eu>
Tue, 27 Oct 2026 14:37:52 +0000 (15:37 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 21:45:32 +0000 (22:45 +0100)
Rejects requests and responses in which one header field name contains
an underscore character ('_' or \x5C). This character is perfectly
permitted, but some application frameworks which create automatic
variables tend to 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 safe to enable on untrusted listeners.

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

index b27da239375515eb4e22060321b4c92de18dd212..e16c8b7dc7083dbd967297d9260ac328ebc22e38 100644 (file)
@@ -34538,6 +34538,16 @@ http-hardening <name> [from <name2>]
         exploit different case matching schemes along the chain. This is
         recommended on untrusted listeners.
 
+  - reject-underscore-in-header-name (inbound, request and response)
+        Rejects requests and responses in which one header field name contains
+        an underscore character ('_' or \x5C). This character is perfectly
+        permitted, but some application frameworks which create automatic
+        variables tend to 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 safe to enable on untrusted listeners.
+
 /*
  * Local variables:
  *  fill-column: 79
index 1d7ec25980d0abe718c01558f4ec8a1f86112305..6a9adfd38b13842552ca37f48e9414db272062a7 100644 (file)
@@ -242,6 +242,7 @@ struct http_violation {
 /* insert new masks below this line */
 #define HTTP_HRD_REJ_DUP_HOST_HDR           0x00000001
 #define HTTP_HRD_REJ_LCASE_METHOD           0x00000002
+#define HTTP_HRD_REJ_USCORE_HDR_NAM         0x00000004
 /* insert new masks above this line */
 #define HTTP_HRD_ALL                        0xFFFFFFFF
 
index 5ed8582470633f2f77d637c78ee703688fe3c9f0..a1541eb8f5ecea895d7e96ff11465d93df2db94a 100644 (file)
--- a/src/h1.c
+++ b/src/h1.c
@@ -856,6 +856,12 @@ int h1_headers_to_hdr_list(char *start, const char *stop,
                                /* turn it to lower case if needed */
                                if (isupper((unsigned char)*ptr) && h1m->flags & H1_MF_TOLOWER)
                                        *ptr = tolower((unsigned char)*ptr);
+                               else if (unlikely(*ptr == '_')) {
+                                       if ((h1m->state != H1_MSG_TRAILERS && http_harden(HTTP_HRD_REJ_USCORE_HDR_NAM))) {
+                                               state = H1_MSG_RQMETH;
+                                               goto http_msg_invalid;
+                                       }
+                               }
                        }
                        EAT_AND_JUMP_OR_RETURN(ptr, end, http_msg_hdr_name, http_msg_ood, state, H1_MSG_HDR_NAME);
                }
index 031db21e5d01f464aceee1b65e7613ec8f2cd40c..fa9486bdb2a04afbc81e8b441b0248acfda64735 100644 (file)
--- a/src/h2.c
+++ b/src/h2.c
@@ -349,9 +349,12 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms
                         */
                        phdr = h2_str_to_phdr(list[idx].n);
 
-                       for (i = !!phdr; i < list[idx].n.len; i++)
+                       for (i = !!phdr; i < list[idx].n.len; i++) {
                                if ((uint8_t)(list[idx].n.ptr[i] - 'A') <= 'Z' - 'A' || !HTTP_IS_TOKEN(list[idx].n.ptr[i]))
                                        goto fail;
+                               if (list[idx].n.ptr[i] == '_' && http_harden(HTTP_HRD_REJ_USCORE_HDR_NAM))
+                                       goto fail;
+                       }
                }
 
                /* RFC7540#10.3: intermediaries forwarding to HTTP/1 must take care of
@@ -679,9 +682,12 @@ int h2_make_htx_response(struct http_hdr *list, struct htx *htx, unsigned int *m
                         */
                        phdr = h2_str_to_phdr(list[idx].n);
 
-                       for (i = !!phdr; i < list[idx].n.len; i++)
+                       for (i = !!phdr; i < list[idx].n.len; i++) {
                                if ((uint8_t)(list[idx].n.ptr[i] - 'A') <= 'Z' - 'A' || !HTTP_IS_TOKEN(list[idx].n.ptr[i]))
                                        goto fail;
+                               if (list[idx].n.ptr[i] == '_' && http_harden(HTTP_HRD_REJ_USCORE_HDR_NAM))
+                                       goto fail;
+                       }
                }
 
                /* RFC7540#10.3: intermediaries forwarding to HTTP/1 must take care of
index 6958977fe19aa2c134bdb592d05ee6db74163f81..3fbfa2758c94a8aa2ea040b9f3d62abe5691b077 100644 (file)
--- a/src/h3.c
+++ b/src/h3.c
@@ -600,6 +600,10 @@ static int _h3_handle_hdr(struct qcs *qcs, const struct http_hdr *hdr)
                        TRACE_ERROR("invalid characters in field name", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
                        goto err;
                }
+               if (c == '_' && http_harden(HTTP_HRD_REJ_USCORE_HDR_NAM)) {
+                       TRACE_ERROR("hardening rejected underscore in field name", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
+                       goto err;
+               }
        }
 
        /* RFC 9114 10.3 Intermediary-Encapsulation Attacks
index 449407003daafd9b93ce0faef63c539fd731a786..cf039af317d7318b9081dadf4331d6b5a91280bf 100644 (file)
@@ -1777,6 +1777,7 @@ 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  },
        { "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 },
        /* insert new hardening options above this line */
        { "all",                            HTTP_HRD_ALL,              HTTP_DIR_BOTH },
        { NULL, 0 }