Просмотр исходного кода

Fix OSS-Fuzz #508342856: cap Content-Length reservation by payload_max_length_

A malicious or malformed server response with an enormous Content-Length
header (e.g. 20000000000) caused the client to call res.body.reserve(len)
with the untrusted value, triggering OOM before read_content's
payload_max_length_ check could take effect. Cap the pre-reservation
at payload_max_length_, since reading more than that is never useful.
yhirose 5 месяцев назад
Родитель
Сommit
2d2efe46da

+ 9 - 1
httplib.h

@@ -13632,7 +13632,15 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
           output_error_log(error, &req);
           return false;
         }
-        res.body.reserve(static_cast<size_t>(len));
+        // Cap the reservation by payload_max_length_ to avoid OOM when a
+        // hostile or malformed server sends an enormous Content-Length.
+        // The actual body read below is bounded by payload_max_length_,
+        // so reserving more than that is never useful.
+        auto reserve_len = static_cast<size_t>(len);
+        if (payload_max_length_ > 0 && reserve_len > payload_max_length_) {
+          reserve_len = payload_max_length_;
+        }
+        res.body.reserve(reserve_len);
       }
     }
 

+ 3 - 0
test/fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-5188033728282624

@@ -0,0 +1,3 @@
+ HTTP/1.1 777
+Content-Length:20000000000
+

+ 75 - 0
test/test.cc

@@ -9288,6 +9288,81 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
       << " bytes without truncation, but only read " << total_read << " bytes.";
 }
 
+// Regression test for OSS-Fuzz issue 508342856: a malicious server sending an
+// enormous Content-Length must not cause the client to pre-allocate a huge
+// response body buffer. The reservation is capped at payload_max_length_, and
+// the read itself fails when the body exceeds the limit.
+TEST(ClientVulnerabilityTest, HugeContentLengthDoesNotPreallocate) {
+#ifndef _WIN32
+  signal(SIGPIPE, SIG_IGN);
+#endif
+
+  auto server_thread = std::thread([] {
+    auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
+    default_socket_options(srv);
+    detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
+    detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
+
+    sockaddr_in addr{};
+    addr.sin_family = AF_INET;
+    addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
+    ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
+
+    int opt = 1;
+    ::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
+#ifdef _WIN32
+                 reinterpret_cast<const char *>(&opt),
+#else
+                 &opt,
+#endif
+                 sizeof(opt));
+
+    ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
+    ::listen(srv, 1);
+
+    sockaddr_in cli_addr{};
+    socklen_t cli_len = sizeof(cli_addr);
+    auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
+
+    if (cli != INVALID_SOCKET) {
+      char buf[4096];
+      ::recv(cli, buf, sizeof(buf), 0);
+
+      // Malicious response: claim a 20GB body but send only a tiny payload.
+      std::string response = "HTTP/1.1 200 OK\r\n"
+                             "Content-Length: 20000000000\r\n"
+                             "\r\n"
+                             "abc";
+      ::send(cli,
+#ifdef _WIN32
+             static_cast<const char *>(response.c_str()),
+             static_cast<int>(response.size()),
+#else
+             response.c_str(), response.size(),
+#endif
+             0);
+
+      detail::close_socket(cli);
+    }
+    detail::close_socket(srv);
+  });
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+  {
+    Client cli("127.0.0.1", PORT + 2);
+    cli.set_read_timeout(5, 0);
+    // Default payload_max_length_ is 100MB; a 20GB Content-Length must not
+    // result in a 20GB pre-allocation. The Get() call is expected to fail
+    // (server claims more bytes than payload_max_length permits), but it must
+    // not exhaust memory before getting there.
+    auto res = cli.Get("/malicious");
+    EXPECT_FALSE(res); // Read fails because body exceeds payload_max_length_
+  }
+
+  server_thread.join();
+}
+
 // Verify that content_receiver bypasses the default payload_max_length,
 // allowing streaming downloads larger than 100MB without requiring an explicit
 // set_payload_max_length call.