This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new fb9413c3 Reject HTTP/1 header forms that desync framing, drop chunked 
trailers (#3521)
fb9413c3 is described below

commit fb9413c310cdab2396fb1ee562dea3c412d18451
Author: Bright Chen <[email protected]>
AuthorDate: Sat Sep 5 16:02:31 2026 +0800

    Reject HTTP/1 header forms that desync framing, drop chunked trailers 
(#3521)
---
 src/brpc/details/http_message.cpp   |  22 ++++
 src/brpc/details/http_parser.cpp    | 112 ++++++++++++++++--
 src/brpc/details/http_parser.h      |   6 +
 test/brpc_http_message_unittest.cpp | 219 +++++++++++++++++++++++++++++++++++-
 4 files changed, 348 insertions(+), 11 deletions(-)

diff --git a/src/brpc/details/http_message.cpp 
b/src/brpc/details/http_message.cpp
index 45f77bab..0cb4f783 100644
--- a/src/brpc/details/http_message.cpp
+++ b/src/brpc/details/http_message.cpp
@@ -33,6 +33,13 @@ namespace brpc {
 
 DEFINE_bool(allow_chunked_length, false,
             "Allow both Transfer-Encoding and Content-Length headers are 
present.");
+DEFINE_bool(http_allow_obs_fold, false,
+            "Allow obsolete line folding (RFC 7230 3.2.4) of Content-Length "
+            "and Transfer-Encoding, which front proxies may parse 
differently.");
+DEFINE_bool(http_strict_header_token, false,
+            "Reject a space inside any header field name (RFC 7230 3.2.6), "
+            "not just inside the names that decide framing. Names carrying no "
+            "framing semantics are accepted by default for compatibility.");
 DEFINE_bool(allow_http_1_1_request_without_host, true,
             "Allow HTTP/1.1 request without host which violates the HTTP/1.1 
specification.");
 DEFINE_bool(http_verbose, false,
@@ -83,6 +90,15 @@ int HttpMessage::on_status(http_parser *parser, const char 
*, const size_t) {
 // change the order of these field values when a message is forwarded. 
 int HttpMessage::on_header_field(http_parser *parser,
                                  const char *at, const size_t length) {
+    if (parser->flags & F_TRAILING) {
+        // Drop chunked trailers instead of merging them into the header map.
+        // A front-end that strips or rewrites headers (Authorization,
+        // X-Forwarded-For, ...) only looks at the header section, so anything
+        // accepted here would be a header smuggled past it. HTTP/1 trailers
+        // have no reader API in bRPC anyway. Supporting them later should add
+        // a separate map rather than reuse this one.
+        return 0;
+    }
     HttpMessage *http_message = (HttpMessage *)parser->data;
     if (http_message->_stage != HTTP_ON_HEADER_FIELD) {
         http_message->_stage = HTTP_ON_HEADER_FIELD;
@@ -94,6 +110,10 @@ int HttpMessage::on_header_field(http_parser *parser,
 
 int HttpMessage::on_header_value(http_parser *parser,
                                  const char *at, const size_t length) {
+    if (parser->flags & F_TRAILING) {
+        // See the comment in on_header_field().
+        return 0;
+    }
     HttpMessage *http_message = (HttpMessage *)parser->data;
     bool first_entry = false;
     if (http_message->_stage != HTTP_ON_HEADER_VALUE) {
@@ -454,6 +474,8 @@ HttpMessage::HttpMessage(bool read_body_progressively,
     , _read_body_progressively(read_body_progressively) {
     http_parser_init(&_parser, HTTP_BOTH);
     _parser.allow_chunked_length = 1;
+    _parser.allow_obs_fold = FLAGS_http_allow_obs_fold;
+    _parser.strict_header_token = FLAGS_http_strict_header_token;
     _parser.data = this;
 }
 
diff --git a/src/brpc/details/http_parser.cpp b/src/brpc/details/http_parser.cpp
index 2c68cffc..e983683d 100644
--- a/src/brpc/details/http_parser.cpp
+++ b/src/brpc/details/http_parser.cpp
@@ -407,6 +407,8 @@ enum header_states
 
   , h_connection
   , h_content_length
+  , h_content_length_num
+  , h_content_length_ws
   , h_transfer_encoding
   , h_upgrade
 
@@ -435,9 +437,13 @@ const char* http_parser_header_state_name(unsigned int 
header_state) {
     case h_matching_upgrade: return "h_matching_upgrade";
     case h_connection: return "h_connection";
     case h_content_length: return "h_content_length";
+    case h_content_length_num: return "h_content_length_num";
+    case h_content_length_ws: return "h_content_length_ws";
     case h_transfer_encoding: return "h_transfer_encoding";
     case h_upgrade: return "h_upgrade";
+    case h_matching_transfer_encoding_token_start: return 
"h_matching_transfer_encoding_token_start";
     case h_matching_transfer_encoding_chunked: return 
"h_matching_transfer_encoding_chunked";
+    case h_matching_transfer_encoding_token: return 
"h_matching_transfer_encoding_token";
     case h_matching_connection_keep_alive: return 
"h_matching_connection_keep_alive";
     case h_matching_connection_close: return "h_matching_connection_close";
     case h_transfer_encoding_chunked: return "h_transfer_encoding_chunked";
@@ -447,6 +453,26 @@ const char* http_parser_header_state_name(unsigned int 
header_state) {
     return "h_unknown";
 }
 
+/* States in which the value being parsed decides where the body ends. Folding
+ * such a value (RFC 7230 3.2.4 obs-fold) is never legitimate and makes us
+ * disagree with front proxies on the framing, which is a smuggling primitive.
+ */
+static int is_framing_header_state(unsigned int header_state) {
+  switch (header_state) {
+  case h_content_length:
+  case h_content_length_num:
+  case h_content_length_ws:
+  case h_transfer_encoding:
+  case h_matching_transfer_encoding_token_start:
+  case h_matching_transfer_encoding_chunked:
+  case h_matching_transfer_encoding_token:
+  case h_transfer_encoding_chunked:
+    return 1;
+  default:
+    return 0;
+  }
+}
+
 const char* http_parser_type_name(enum http_parser_type type) {
     switch (type) {
     case HTTP_REQUEST: return "HTTP_REQUEST";
@@ -477,6 +503,10 @@ enum http_host_state
 #define IS_ALPHA(c)         (LOWER(c) >= 'a' && LOWER(c) <= 'z')
 #define IS_NUM(c)           ((c) >= '0' && (c) <= '9')
 #define IS_ALPHANUM(c)      (IS_ALPHA(c) || IS_NUM(c))
+/* RFC 7230 3.2.3: OWS = *( SP / HTAB ). Pass the raw byte, never the LOWER()ed
+ * one: LOWER('\t') is ')', so a tab never compares equal to '\t' afterwards.
+ */
+#define IS_OWS(ch)          ((ch) == ' ' || (ch) == '\t')
 #define IS_HEX(c)           (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f'))
 #define IS_MARK(c)          ((c) == '-' || (c) == '_' || (c) == '.' || \
   (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \
@@ -489,6 +519,12 @@ enum http_host_state
  * characeters seem to be OK.
  */
 #define TOKEN(c)            ((c == ' ') ? ' ' : tokens[(unsigned char)c])
+/* tokens[] already leaves SP out, as RFC 7230 3.2.6 does, so the strict form 
is
+ * the plain lookup and TOKEN() is the leniency layered on top of it. The name
+ * matches upstream, where the table keeps SP and STRICT_TOKEN() is what drops
+ * it, so both forks spell the same two behaviours the same way.
+ */
+#define STRICT_TOKEN(c)     (tokens[(unsigned char)c])
 #define IS_HOST_CHAR(c)                                         \
   (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_')
 /* NOTE(gejun): Enable utf8 which is compatible with ASCII. Services in baidu
@@ -702,6 +738,8 @@ size_t http_parser_execute (http_parser *parser,
   const char *status_mark = 0;
   const unsigned int lenient = parser->lenient_http_headers;
   const unsigned int allow_chunked_length = parser->allow_chunked_length;
+  const unsigned int allow_obs_fold = parser->allow_obs_fold;
+  const unsigned int strict_header_token = parser->strict_header_token;
 
   /* We're in an error state. Don't bother doing anything. */
   if (HTTP_PARSER_ERRNO(parser) != HPE_OK) {
@@ -1369,7 +1407,7 @@ size_t http_parser_execute (http_parser *parser,
           goto reexecute_byte;
         }
 
-        c = TOKEN(ch);
+        c = strict_header_token ? STRICT_TOKEN(ch) : TOKEN(ch);
 
         if (!c) {
           SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
@@ -1407,7 +1445,7 @@ size_t http_parser_execute (http_parser *parser,
 
       case s_header_field:
       {
-        c = TOKEN(ch);
+        c = strict_header_token ? STRICT_TOKEN(ch) : TOKEN(ch);
 
         if (c) {
           switch (parser->header_state) {
@@ -1504,7 +1542,19 @@ size_t http_parser_execute (http_parser *parser,
             case h_content_length:
             case h_transfer_encoding:
             case h_upgrade:
-              if (ch != ' ') parser->header_state = h_general;
+              /* TOKEN() accepts ' ' (see the NOTE above it), so without this
+               * `Content-Length : 5` would keep header_state and be used for
+               * framing while the field name we report is "Content-Length ".
+               * RFC 7230 3.2.4 forbids whitespace before the colon; front
+               * proxies that follow it disagree with us, which is a smuggling
+               * primitive. Only these four names are strict, lenient names
+               * elsewhere stay accepted.
+               */
+              if (ch == ' ') {
+                SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+                goto error;
+              }
+              parser->header_state = h_general;
               break;
 
             default:
@@ -1580,6 +1630,12 @@ size_t http_parser_execute (http_parser *parser,
 
             parser->flags |= F_CONTENTLENGTH;
             parser->content_length = ch - '0';
+            parser->header_state = h_content_length_num;
+            break;
+
+          /* when obsolete line folding is encountered for content length
+           * continue to the s_header_value state */
+          case h_content_length_ws:
             break;
 
           case h_connection:
@@ -1633,10 +1689,18 @@ size_t http_parser_execute (http_parser *parser,
             break;
 
           case h_content_length:
+            if (IS_OWS(ch)) break;
+            parser->header_state = h_content_length_num;
+            /* FALLTHROUGH */
+
+          case h_content_length_num:
           {
             uint64_t t;
 
-            if (ch == ' ') break;
+            if (IS_OWS(ch)) {
+              parser->header_state = h_content_length_ws;
+              break;
+            }
 
             if (!IS_NUM(ch)) {
               SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
@@ -1657,14 +1721,25 @@ size_t http_parser_execute (http_parser *parser,
             break;
           }
 
+          /* Trailing OWS ends the number. A digit after it means the value was
+           * something like "1 3", which we used to read as 13 while proxies 
may
+           * read 1 or reject the message. cf. CVE-2022-32213.
+           */
+          case h_content_length_ws:
+            if (IS_OWS(ch)) break;
+            SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+            goto error;
+
           /* Transfer-Encoding: chunked */
           case h_matching_transfer_encoding_token_start:
             /* looking for 'Transfer-Encoding: chunked' */
             if ('c' == c) {
               parser->header_state = h_matching_transfer_encoding_chunked;
-            } else if (TOKEN(c)) {
-              /* NOTE(gejun): Not use strict mode for these macros since the 
additional
-               * characeters seem to be OK.
+            } else if (STRICT_TOKEN(c)) {
+              /* TOKEN() here made the OWS arm below unreachable for SP, so the
+               * space in `Transfer-Encoding: gzip, chunked` started a token
+               * instead of being skipped and chunked was never matched. Use 
the
+               * strict form unconditionally, as upstream does.
                */
 
               /* TODO(indutny): similar code below does this, but why?
@@ -1673,7 +1748,7 @@ size_t http_parser_execute (http_parser *parser,
                * `STRICT_TOKEN`
                */
               parser->header_state = h_matching_transfer_encoding_token;
-            } else if (c == ' ' || c == '\t') {
+            } else if (IS_OWS(ch)) {
               /* Skip lws */
             } else {
               parser->header_state = h_general;
@@ -1720,12 +1795,12 @@ size_t http_parser_execute (http_parser *parser,
             break;
 
           case h_transfer_encoding_chunked:
-            if (ch != ' ') parser->header_state = 
h_matching_transfer_encoding_token;
+            if (!IS_OWS(ch)) parser->header_state = 
h_matching_transfer_encoding_token;
             break;
 
           case h_connection_keep_alive:
           case h_connection_close:
-            if (ch != ' ') parser->header_state = h_general;
+            if (!IS_OWS(ch)) parser->header_state = h_general;
             break;
 
           default:
@@ -1747,6 +1822,14 @@ size_t http_parser_execute (http_parser *parser,
       case s_header_value_lws:
       {
         if (ch == ' ' || ch == '\t') {
+          if (!allow_obs_fold && 
is_framing_header_state(parser->header_state)) {
+            SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+            goto error;
+          }
+          if (parser->header_state == h_content_length_num) {
+            /* treat obsolete line folding as space */
+            parser->header_state = h_content_length_ws;
+          }
           parser->state = s_header_value_start;
           goto reexecute_byte;
         }
@@ -1780,6 +1863,15 @@ size_t http_parser_execute (http_parser *parser,
       case s_header_value_discard_lws:
       {
         if (ch == ' ' || ch == '\t') {
+          /* The value is still empty, so `Transfer-Encoding:\r\n chunked` and
+           * `Content-Length:\r\n 5` come through here rather than through
+           * s_header_value_lws. Both states keep header_state across the fold,
+           * so both have to refuse it.
+           */
+          if (!allow_obs_fold && 
is_framing_header_state(parser->header_state)) {
+            SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+            goto error;
+          }
           parser->state = s_header_value_discard_ws;
           break;
         } else {
diff --git a/src/brpc/details/http_parser.h b/src/brpc/details/http_parser.h
index 0ea4efd0..b09c912b 100644
--- a/src/brpc/details/http_parser.h
+++ b/src/brpc/details/http_parser.h
@@ -216,6 +216,12 @@ struct http_parser {
                                           * `Content-Length` and
                                           * `Transfer-Encoding: chunked` set */
   unsigned int lenient_http_headers : 1;
+  unsigned int allow_obs_fold : 1; /* Allow obsolete line folding (RFC 7230
+                                    * 3.2.4) of `Content-Length` and
+                                    * `Transfer-Encoding` */
+  unsigned int strict_header_token : 1; /* Reject SP inside any header field
+                                         * name, not just the ones that decide
+                                         * framing (RFC 7230 3.2.6) */
 
   uint32_t nread;          /* # bytes read in various scenarios */
   uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one)
diff --git a/test/brpc_http_message_unittest.cpp 
b/test/brpc_http_message_unittest.cpp
index 951c9108..57e98cca 100644
--- a/test/brpc_http_message_unittest.cpp
+++ b/test/brpc_http_message_unittest.cpp
@@ -18,6 +18,7 @@
 // Date 2014/10/24 16:44:30
 
 #include <gtest/gtest.h>
+#include <gflags/gflags.h>
 #include <google/protobuf/descriptor.h>
 
 #include "brpc/server.h"
@@ -29,6 +30,8 @@ namespace brpc {
 
 DECLARE_bool(allow_chunked_length);
 DECLARE_bool(allow_http_1_1_request_without_host);
+DECLARE_bool(http_allow_obs_fold);
+DECLARE_bool(http_strict_header_token);
 
 int main(int argc, char* argv[]) {
     testing::InitGoogleTest(&argc, argv);
@@ -340,6 +343,8 @@ TEST(HttpMessageTest, parse_http_set_cookie) {
 }
 
 TEST(HttpMessageTest, cl_and_te) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+
     // https://datatracker.ietf.org/doc/html/rfc2616#section-14.41
     // If multiple encodings have been applied to an entity, the transfer-
     // codings MUST be listed in the order in which they were applied.
@@ -426,6 +431,218 @@ TEST(HttpMessageTest, cl_and_te) {
     }
 }
 
+brpc::http_errno ParseHttpErrno(const char* buf) {
+    butil::IOBuf data;
+    data.append(buf);
+    brpc::HttpMessage http_message;
+    if (http_message.ParseFromIOBuf(data) == (ssize_t)data.size() &&
+        http_message.Completed()) {
+        return brpc::HPE_OK;
+    }
+    brpc::http_errno err = (brpc::http_errno)http_message._parser.http_errno;
+    return err != brpc::HPE_OK ? err : brpc::HPE_UNKNOWN;
+}
+
+TEST(HttpMessageTest, space_before_colon_of_framing_headers) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_strict_header_token = false;
+
+    const char* rejected[] = {
+        "POST / HTTP/1.1\r\nHost: a.com\r\nContent-Length : 5\r\n\r\nhello",
+        "POST / HTTP/1.1\r\nHost: a.com\r\nTransfer-Encoding : 
chunked\r\n\r\n0\r\n\r\n",
+        "GET / HTTP/1.1\r\nHost: a.com\r\nConnection : close\r\n\r\n",
+        "GET / HTTP/1.1\r\nHost: a.com\r\nUpgrade : h2c\r\n\r\n",
+    };
+    for (size_t i = 0; i < arraysize(rejected); ++i) {
+        ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, ParseHttpErrno(rejected[i]))
+            << rejected[i];
+    }
+
+    // Names without framing semantics keep the historical leniency.
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno(
+        "POST / HTTP/1.1\r\nHost: a.com\r\nX-Foo : bar\r\n"
+        "Content-Length: 5\r\n\r\nhello"));
+}
+
+TEST(HttpMessageTest, strict_header_token) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_strict_header_token = false;
+
+    const char* lenient_only[] = {
+        "POST / HTTP/1.1\r\nHost: a.com\r\nX-Foo : bar\r\n"
+        "Content-Length: 5\r\n\r\nhello",
+        // A space anywhere in the name, not just before the colon.
+        "GET / HTTP/1.1\r\nHost: a.com\r\nX Foo: bar\r\n\r\n",
+        // Names that only look like a framing one until they diverge.
+        "GET / HTTP/1.1\r\nHost: a.com\r\nContent-Type : text/plain\r\n\r\n",
+    };
+    for (size_t i = 0; i < arraysize(lenient_only); ++i) {
+        ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno(lenient_only[i]))
+            << lenient_only[i];
+    }
+
+    brpc::FLAGS_http_strict_header_token = true;
+    for (size_t i = 0; i < arraysize(lenient_only); ++i) {
+        ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN,
+                  ParseHttpErrno(lenient_only[i])) << lenient_only[i];
+    }
+    // Well-formed names are unaffected.
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno("POST / HTTP/1.1\r\n"
+                                           "Host: a.com\r\nX-Foo: bar\r\n"
+                                           "Content-Length: 5\r\n\r\n"
+                                           "hello"));
+}
+
+TEST(HttpMessageTest, transfer_encoding_list_with_space_after_comma) {
+    butil::IOBuf response;
+    response.append("HTTP/1.1 200 OK\r\n"
+                    "Transfer-Encoding: gzip, chunked\r\n\r\n"
+                    "5\r\nhello\r\n0\r\n\r\n");
+    brpc::HttpMessage http_message;
+    ASSERT_EQ((ssize_t)response.size(), http_message.ParseFromIOBuf(response))
+        << http_message._parser;
+    ASSERT_TRUE(http_message.Completed());
+    ASSERT_TRUE(http_message._parser.flags & brpc::F_CHUNKED)
+        << http_message._parser;
+    ASSERT_EQ("hello", http_message.body().to_string());
+
+    // A request would have been rejected outright by RFC 7230 3.3.3 before,
+    // because uses_transfer_encoding was set while F_CHUNKED was not.
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno("POST / HTTP/1.1\r\n"
+                                           "Host: a.com\r\n"
+                                           "Transfer-Encoding: gzip, 
chunked\r\n\r\n"
+                                           "0\r\n\r\n"));
+}
+
+TEST(HttpMessageTest, content_length_with_interior_space) {
+    ASSERT_EQ(brpc::HPE_INVALID_CONTENT_LENGTH, ParseHttpErrno("POST / 
HTTP/1.1\r\n"
+                                                               "Host: 
a.com\r\n"
+                                                               
"Content-Length: 1 3\r\n\r\n"
+                                                               
"0123456789abc"));
+
+    // Trailing OWS is legal and does not change the value.
+    butil::IOBuf data;
+    data.append("POST / HTTP/1.1\r\nHost: a.com\r\nContent-Length: 13 
\r\n\r\n0123456789abc");
+    brpc::HttpMessage http_message;
+    ASSERT_EQ((ssize_t)data.size(), http_message.ParseFromIOBuf(data))
+        << http_message._parser;
+    ASSERT_TRUE(http_message.Completed());
+    ASSERT_EQ("0123456789abc", http_message.body().to_string());
+}
+
+TEST(HttpMessageTest, obs_fold_of_framing_headers) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+    brpc::FLAGS_http_allow_obs_fold = false;
+
+    const char* folded_te_value = "POST / HTTP/1.1\r\n"
+                                  "Host: a.com\r\n"
+                                  "Transfer-Encoding:\r\n chunked\r\n\r\n"
+                                  "0\r\n\r\n";
+    // Folding inside the value restarts the chunked matcher, so this one never
+    // reached F_CHUNKED, but we still report "chun ked" where a proxy that
+    // unfolds reports "chunked".
+    const char* folded_te_token = "GET / HTTP/1.1\r\n"
+                                  "Host: a.com\r\n"
+                                  "Transfer-Encoding: chun\r\n ked\r\n\r\n";
+    const char* folded_cl_value = "POST / HTTP/1.1\r\n"
+                                  "Host: a.com\r\n"
+                                  "Content-Length:\r\n 13\r\n\r\n"
+                                  "0123456789abc";
+    const char* folded_cl_digits = "POST / HTTP/1.1\r\n"
+                                   "Host: a.com\r\n"
+                                   "Content-Length: 1\r\n 3\r\n\r\n"
+                                   "0123456789abc";
+
+    ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, ParseHttpErrno(folded_te_value));
+    ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, ParseHttpErrno(folded_te_token));
+    ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, ParseHttpErrno(folded_cl_value));
+    ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, 
ParseHttpErrno(folded_cl_digits));
+
+    // Folding a header that decides nothing about framing is still accepted.
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno(
+        "POST / HTTP/1.1\r\nHost: a.com\r\nX-Foo: a\r\n b\r\n"
+        "Content-Length: 5\r\n\r\nhello"));
+
+    brpc::FLAGS_http_allow_obs_fold = true;
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno(folded_te_value));
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno(folded_cl_value));
+    // Unfolding turns this into `Content-Length: 1 3`, which stays invalid.
+    ASSERT_EQ(brpc::HPE_INVALID_CONTENT_LENGTH,
+              ParseHttpErrno(folded_cl_digits));
+    // Unfolding leaves a Transfer-Encoding that is not chunked, which RFC 7230
+    // 3.3.3 makes unparseable in a request whatever the fold policy is.
+    ASSERT_EQ(brpc::HPE_INVALID_TRANSFER_ENCODING,
+              ParseHttpErrno(folded_te_token));
+}
+
+TEST(HttpMessageTest, chunked_trailer_is_not_a_header) {
+    butil::IOBuf request;
+    request.append("POST / HTTP/1.1\r\n"
+                   "Host: a.com\r\n"
+                   "X-Forwarded-For: 10.0.0.1\r\n"
+                   "Transfer-Encoding: chunked\r\n\r\n"
+                   "5\r\nhello\r\n"
+                   "0\r\n"
+                   "X-Injected: evil\r\n"
+                   "Authorization: Bearer stolen\r\n"
+                   "X-Forwarded-For: 6.6.6.6\r\n"
+                   "\r\n");
+    brpc::HttpMessage http_message;
+    ASSERT_EQ((ssize_t)request.size(), http_message.ParseFromIOBuf(request))
+        << http_message._parser;
+    ASSERT_TRUE(http_message.Completed());
+    // The body is unaffected by dropping the trailer.
+    ASSERT_EQ("hello", http_message.body().to_string());
+
+    brpc::HttpHeader& header = http_message.header();
+    ASSERT_EQ(nullptr, header.GetHeader("X-Injected"));
+    ASSERT_EQ(nullptr, header.GetHeader("Authorization"));
+    // A trailer repeating a real header must not be appended to it either,
+    // which is where a comma-folded name like X-Forwarded-For would land.
+    const std::string* xff = header.GetHeader("X-Forwarded-For");
+    ASSERT_TRUE(xff != nullptr);
+    ASSERT_EQ("10.0.0.1", *xff);
+}
+
+TEST(HttpMessageTest, htab_is_ows_in_header_values) {
+    // Trailing OWS after the number, alone and mixed with SP.
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno("POST / HTTP/1.1\r\n"
+                                           "Host: a.com\r\n"
+                                           "Content-Length: 13\t\r\n\r\n"
+                                           "0123456789abc"));
+    ASSERT_EQ(brpc::HPE_OK, ParseHttpErrno("POST / HTTP/1.1\r\n"
+                                           "Host: a.com\r\n"
+                                           "Content-Length: 13 \t\r\n\r\n"
+                                           "0123456789abc"));
+    // A tab between digits ends the number just like a space, so the value
+    // stays invalid rather than reading as 13.
+    ASSERT_EQ(brpc::HPE_INVALID_CONTENT_LENGTH,
+              ParseHttpErrno("POST / HTTP/1.1\r\n"
+                             "Host: a.com\r\n"
+                             "Content-Length: 1\t3\r\n\r\n"
+                             "0123456789abc"));
+
+    // Tabs around a transfer-coding and after `Connection: close` used to 
leave
+    // the matcher in a state that never set the flag. The Connection case
+    // parsed without an error, so only the flag catches it.
+    struct { const char* buf; unsigned int flag; } flagged[] = {
+        {"POST / HTTP/1.1\r\nHost: a.com\r\n"
+         "Transfer-Encoding: gzip,\tchunked\r\n\r\n0\r\n\r\n", 
brpc::F_CHUNKED},
+        {"POST / HTTP/1.1\r\nHost: a.com\r\n"
+         "Transfer-Encoding: chunked\t\r\n\r\n0\r\n\r\n", brpc::F_CHUNKED},
+        {"GET / HTTP/1.1\r\nHost: a.com\r\nConnection: close\t\r\n\r\n", 
brpc::F_CONNECTION_CLOSE},
+    };
+    for (size_t i = 0; i < arraysize(flagged); ++i) {
+        butil::IOBuf data;
+        data.append(flagged[i].buf);
+        brpc::HttpMessage http_message;
+        ASSERT_EQ((ssize_t)data.size(), http_message.ParseFromIOBuf(data))
+            << http_message._parser;
+        ASSERT_TRUE(http_message._parser.flags & flagged[i].flag)
+            << http_message._parser;
+    }
+}
+
 TEST(HttpMessageTest, find_method_property_by_uri) {
     brpc::Server server;
     ASSERT_EQ(0, server.AddService(new test::EchoService(),
@@ -799,6 +1016,7 @@ TEST(HttpMessageTest, 
serialize_content_type_with_crlf_is_not_injected) {
 }
 
 TEST(HttpMessageTest, http_1_1_request_without_host) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
     brpc::FLAGS_allow_http_1_1_request_without_host = false;
     {
         butil::IOBuf request;
@@ -830,7 +1048,6 @@ TEST(HttpMessageTest, http_1_1_request_without_host) {
         ASSERT_TRUE(http_message.Completed());
         ASSERT_EQ("text/plain", http_message.header().content_type());
     }
-    brpc::FLAGS_allow_http_1_1_request_without_host = true;
 }
 
 } //namespace


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to