]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http/hardening: implement hardening "reject-lowercase-method"
authorWilly Tarreau <w@1wt.eu>
Tue, 27 Oct 2026 14:16:23 +0000 (15:16 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 21:45:32 +0000 (22:45 +0100)
Rejects requests whose method contains a lower-case character. No
standard HTTP method uses lower-case characters and these ones are
essentially used in desynchronization attack scenarios, by trying to
exploit different case matching schemes along the chain. This is
recommended on untrusted listeners. This is implemented directly in the
h1 parser, as well as in http_method_has_forbidden_char() for h2 and
h3. H2 now uses this function instead of rolling its own loop.

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

index 0a2fc19ae450e19bafe9e589ef099dfc5cc628df..b27da239375515eb4e22060321b4c92de18dd212 100644 (file)
@@ -34531,6 +34531,13 @@ http-hardening <name> [from <name2>]
         even if not rejected, duplicate host header fields are folded into a
         single one.
 
+  - reject-lowercase-method (inbound request)
+        Rejects requests whose method contains a lower-case character. No
+        standard HTTP method uses lower-case characters and these ones are
+        essentially used in desynchronization attack scenarios, by trying to
+        exploit different case matching schemes along the chain. This is
+        recommended on untrusted listeners.
+
 /*
  * Local variables:
  *  fill-column: 79
index 5fb443c02bc3a08ac79f390179b011119c2005c7..1d7ec25980d0abe718c01558f4ec8a1f86112305 100644 (file)
@@ -241,6 +241,7 @@ struct http_violation {
 #define HTTP_HRD_NONE                       0x00000000
 /* insert new masks below this line */
 #define HTTP_HRD_REJ_DUP_HOST_HDR           0x00000001
+#define HTTP_HRD_REJ_LCASE_METHOD           0x00000002
 /* insert new masks above this line */
 #define HTTP_HRD_ALL                        0xFFFFFFFF
 
index 33b73df0001553cfcc859cdb794bd6facbc865d5..2f70a771908f87a824796f7c4f01bfe7c8e3b66c 100644 (file)
@@ -235,8 +235,8 @@ static inline int http_header_has_forbidden_char(const struct ist ist, const cha
        return 0;
 }
 
-/* Check that method only contains token as required.
- * See RFC 9110 9. Methods
+/* Check that method only contains token as required. See RFC 9110 9. Methods.
+ * Can also enforce non-lower case if hardening requires it.
  */
 static inline int http_method_has_forbidden_char(const struct ist ist)
 {
@@ -245,6 +245,11 @@ static inline int http_method_has_forbidden_char(const struct ist ist)
        do {
                if (!HTTP_IS_TOKEN(*start))
                        return 1;
+
+               if (unlikely(islower((unsigned char)*start))) {
+                       if (http_harden(HTTP_HRD_REJ_LCASE_METHOD))
+                               return 1;
+               }
                start++;
        } while (start < istend(ist));
        return 0;
index 3d3e40068dbfb313a239645d750ed12e5b691972..5ed8582470633f2f77d637c78ee703688fe3c9f0 100644 (file)
--- a/src/h1.c
+++ b/src/h1.c
@@ -509,8 +509,15 @@ int h1_headers_to_hdr_list(char *start, const char *stop,
 
        case H1_MSG_RQMETH:
        http_msg_rqmeth:
-               if (likely(HTTP_IS_TOKEN(*ptr)))
+               if (likely(HTTP_IS_TOKEN(*ptr))) {
+                       if (unlikely(islower((unsigned char)*ptr))) {
+                               if (http_harden(HTTP_HRD_REJ_LCASE_METHOD)) {
+                                       state = H1_MSG_RQMETH;
+                                       goto http_msg_invalid;
+                               }
+                       }
                        EAT_AND_JUMP_OR_RETURN(ptr, end, http_msg_rqmeth, http_msg_ood, state, H1_MSG_RQMETH);
+               }
 
                if (likely(HTTP_IS_SPHT(*ptr))) {
                        sl.rq.m.len = ptr - sl.rq.m.ptr;
index d67f363fcfdd6e2e621d859efd34ab6354a9b707..031db21e5d01f464aceee1b65e7613ec8f2cd40c 100644 (file)
--- a/src/h2.c
+++ b/src/h2.c
@@ -255,12 +255,8 @@ static struct htx_sl *h2_prepare_htx_reqline(uint32_t fields, struct ist *phdr,
        }
 
        /* The method is a non-empty token (RFC7231#4.1) */
-       if (!meth_sl.len)
-               goto fail;
-       for (i = 0; i < meth_sl.len; i++) {
-               if (!HTTP_IS_TOKEN(meth_sl.ptr[i]))
-                       htx->flags |= HTX_FL_PARSING_ERROR;
-       }
+       if (!istlen(meth_sl) || http_method_has_forbidden_char(meth_sl))
+               htx->flags |= HTX_FL_PARSING_ERROR;
 
        /* make sure the final URI isn't empty. Note that 7540#8.1.2.3 states
         * that :path must not be empty.
index 3113c43273a703b93ceb0583812c948b89aa42d2..449407003daafd9b93ce0faef63c539fd731a786 100644 (file)
@@ -1776,6 +1776,7 @@ static int http_parse_acceptable_http_violations(char **args, int section_type,
 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  },
        /* insert new hardening options above this line */
        { "all",                            HTTP_HRD_ALL,              HTTP_DIR_BOTH },
        { NULL, 0 }