MEDIUM: http-ana: Add a proxy option to restrict chars in request header names

The "http-restrict-req-hdr-names" option can now be set to restrict allowed
characters in the request header names to the "[a-zA-Z0-9-]" charset.

Idea of this option is to not send header names with non-alphanumeric or
hyphen character. It is especially important for FastCGI application because
all those characters are converted to underscore. For instance,
"X-Forwarded-For" and "X_Forwarded_For" are both converted to
"HTTP_X_FORWARDED_FOR". So, header names can be mixed up by FastCGI
applications. And some HAProxy rules may be bypassed by mangling header
names. In addition, some non-HTTP compliant servers may incorrectly handle
requests when header names contain characters ouside the "[a-zA-Z0-9-]"
charset.

When this option is set, the policy must be specify:

  * preserve: It disables the filtering. It is the default mode for HTTP
              proxies with no FastCGI application configured.

  * delete: It removes request headers with a name containing a character
            outside the "[a-zA-Z0-9-]" charset. It is the default mode for
            HTTP backends with a configured FastCGI application.

  * reject: It rejects the request with a 403-Forbidden response if it
            contains a header name with a character outside the
            "[a-zA-Z0-9-]" charset.

The option is evaluated per-proxy and after http-request rules evaluation.

This patch may be backported to avoid any secuirty issue with FastCGI
application (so as far as 2.2).

(cherry picked from commit 18c13d3bd88cbcc351a61b1e71881353ab720f67)
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
(cherry picked from commit bf65f308da8b2e6d82d2fb2b242d4bb8f82778d0)
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
diff --git a/src/http_ana.c b/src/http_ana.c
index 48b25c3..1029d85 100644
--- a/src/http_ana.c
+++ b/src/http_ana.c
@@ -58,6 +58,7 @@
 
 static enum rule_result http_req_get_intercept_rule(struct proxy *px, struct list *rules, struct stream *s);
 static enum rule_result http_res_get_intercept_rule(struct proxy *px, struct list *rules, struct stream *s);
+static enum rule_result http_req_restrict_header_names(struct stream *s, struct htx *htx, struct proxy *px);
 
 static void http_manage_client_side_cookies(struct stream *s, struct channel *req);
 static void http_manage_server_side_cookies(struct stream *s, struct channel *res);
@@ -394,6 +395,12 @@
 		}
 	}
 
+	if (px->options2 & (PR_O2_RSTRICT_REQ_HDR_NAMES_BLK|PR_O2_RSTRICT_REQ_HDR_NAMES_DEL)) {
+		verdict = http_req_restrict_header_names(s, htx, px);
+		if (verdict == HTTP_RULE_RES_DENY)
+			goto deny;
+	}
+
 	if (conn && (conn->flags & CO_FL_EARLY_DATA) &&
 	    (conn->flags & (CO_FL_EARLY_SSL_HS | CO_FL_SSL_WAIT_HS))) {
 		struct http_hdr_ctx ctx;
@@ -2595,6 +2602,50 @@
 	return 0;
 }
 
+/* This function filters the request header names to only allow [0-9a-zA-Z-]
+ * characters. Depending on the proxy configuration, headers with a name not
+ * matching this charset are removed or the request is rejected with a
+ * 403-Forbidden response if such name are found. It returns HTTP_RULE_RES_CONT
+ * to continue the request processing or HTTP_RULE_RES_DENY if the request is
+ * rejected.
+ */
+static enum rule_result http_req_restrict_header_names(struct stream *s, struct htx *htx, struct proxy *px)
+{
+	struct htx_blk *blk;
+	enum rule_result rule_ret = HTTP_RULE_RES_CONT;
+
+	blk = htx_get_first_blk(htx);
+	while (blk) {
+		enum htx_blk_type type = htx_get_blk_type(blk);
+
+		if (type == HTX_BLK_HDR) {
+			struct ist n = htx_get_blk_name(htx, blk);
+			int i;
+
+			for (i = 0; i < istlen(n); i++) {
+				if (!isalnum((unsigned char)n.ptr[i]) && n.ptr[i] != '-') {
+					/* Block the request or remove the header */
+					if (px->options2 & PR_O2_RSTRICT_REQ_HDR_NAMES_BLK)
+						goto block;
+					blk = htx_remove_blk(htx, blk);
+					continue;
+				}
+			}
+		}
+		if (type == HTX_BLK_EOH)
+			break;
+
+		blk = htx_get_next_blk(htx, blk);
+	}
+  out:
+	return rule_ret;
+  block:
+	/* Block the request returning a 403-Forbidden response */
+	s->txn->status = 403;
+	rule_ret = HTTP_RULE_RES_DENY;
+	goto out;
+}
+
 /* Replace all headers matching the name <name>. The header value is replaced if
  * it matches the regex <re>. <str> is used for the replacement. If <full> is
  * set to 1, the full-line is matched and replaced. Otherwise, comma-separated