From: Willy Tarreau Date: Tue, 27 Oct 2026 14:37:52 +0000 (+0100) Subject: MINOR: http/hardening: implement hardening "reject-underscore-in-header-name" X-Git-Tag: v3.5-dev11~20 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=72dad7ea11b0ebac6c088d7109d929bd85ad52a3;p=haproxy.git MINOR: http/hardening: implement hardening "reject-underscore-in-header-name" 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. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index b27da2393..e16c8b7dc 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -34538,6 +34538,16 @@ http-hardening [from ] 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 diff --git a/include/haproxy/http-t.h b/include/haproxy/http-t.h index 1d7ec2598..6a9adfd38 100644 --- a/include/haproxy/http-t.h +++ b/include/haproxy/http-t.h @@ -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 diff --git a/src/h1.c b/src/h1.c index 5ed858247..a1541eb8f 100644 --- 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); } diff --git a/src/h2.c b/src/h2.c index 031db21e5..fa9486bdb 100644 --- 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 diff --git a/src/h3.c b/src/h3.c index 6958977fe..3fbfa2758 100644 --- 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 diff --git a/src/http.c b/src/http.c index 449407003..cf039af31 100644 --- a/src/http.c +++ b/src/http.c @@ -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 }