yhirose há 1 mês atrás
pai
commit
f80864ca03
2 ficheiros alterados com 343 adições e 1 exclusões
  1. 10 1
      httplib.h
  2. 333 0
      test/test.cc

+ 10 - 1
httplib.h

@@ -1956,6 +1956,7 @@ protected:
   bool decompress_ = true;
 
   size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  bool has_payload_max_length_ = false;
 
   std::string interface_;
 
@@ -10758,6 +10759,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
   compress_ = rhs.compress_;
   decompress_ = rhs.decompress_;
   payload_max_length_ = rhs.payload_max_length_;
+  has_payload_max_length_ = rhs.has_payload_max_length_;
   interface_ = rhs.interface_;
   proxy_host_ = rhs.proxy_host_;
   proxy_port_ = rhs.proxy_port_;
@@ -12147,7 +12149,10 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
 
     if (res.status != StatusCode::NotModified_304) {
       int dummy_status;
-      if (!detail::read_content(strm, res, payload_max_length_, dummy_status,
+      auto max_length = (!has_payload_max_length_ && req.content_receiver)
+                            ? (std::numeric_limits<size_t>::max)()
+                            : payload_max_length_;
+      if (!detail::read_content(strm, res, max_length, dummy_status,
                                 std::move(progress), std::move(out),
                                 decompress_)) {
         if (error != Error::Canceled) { error = Error::Read; }
@@ -13098,6 +13103,7 @@ inline void ClientImpl::set_decompress(bool on) { decompress_ = on; }
 
 inline void ClientImpl::set_payload_max_length(size_t length) {
   payload_max_length_ = length;
+  has_payload_max_length_ = true;
 }
 
 inline void ClientImpl::set_interface(const std::string &intf) {
@@ -15257,6 +15263,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
   }
 
   auto ssl = static_cast<SSL *>(session);
+  constexpr auto max_len =
+      static_cast<size_t>((std::numeric_limits<int>::max)());
+  if (len > max_len) { len = max_len; }
   auto ret = SSL_read(ssl, buf, static_cast<int>(len));
 
   if (ret > 0) {

+ 333 - 0
test/test.cc

@@ -8752,6 +8752,339 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
       << " bytes without truncation, but only read " << total_read << " bytes.";
 }
 
+// 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.
+TEST(ClientVulnerabilityTest, ContentReceiverBypassesDefaultPayloadMaxLength) {
+  static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
+
+#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(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);
+
+      // Response with Content-Length larger than default 100MB limit
+      auto content_length = std::to_string(DATA_SIZE);
+      std::string response_header = "HTTP/1.1 200 OK\r\n"
+                                    "Content-Length: " +
+                                    content_length +
+                                    "\r\n"
+                                    "Connection: close\r\n"
+                                    "\r\n";
+
+      ::send(cli,
+#ifdef _WIN32
+             static_cast<const char *>(response_header.c_str()),
+             static_cast<int>(response_header.size()),
+#else
+             response_header.c_str(), response_header.size(),
+#endif
+             0);
+
+      std::string chunk(64 * 1024, 'A');
+      size_t total_sent = 0;
+
+      while (total_sent < DATA_SIZE) {
+        auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
+        auto sent = ::send(cli,
+#ifdef _WIN32
+                           static_cast<const char *>(chunk.c_str()),
+                           static_cast<int>(to_send),
+#else
+                           chunk.c_str(), to_send,
+#endif
+                           0);
+        if (sent <= 0) break;
+        total_sent += static_cast<size_t>(sent);
+      }
+
+      detail::close_socket(cli);
+    }
+    detail::close_socket(srv);
+  });
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+  size_t total_received = 0;
+
+  {
+    Client cli("127.0.0.1", PORT + 2);
+    cli.set_read_timeout(10, 0);
+    // Do NOT call set_payload_max_length — use the default 100MB limit
+
+    auto res =
+        cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
+          total_received += data_length;
+          return true;
+        });
+
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+  }
+
+  server_thread.join();
+
+  EXPECT_EQ(total_received, DATA_SIZE)
+      << "With content_receiver, the client should read all " << DATA_SIZE
+      << " bytes despite the default 100MB payload_max_length, but only read "
+      << total_received << " bytes.";
+}
+
+// Verify that an explicit set_payload_max_length smaller than the response is
+// enforced even when a content_receiver is used.
+TEST(ClientVulnerabilityTest,
+     ContentReceiverRespectsExplicitPayloadMaxLength150MB) {
+  static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
+  static constexpr size_t EXPLICIT_LIMIT = 150 * 1024 * 1024; // 150MB limit
+
+#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(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);
+
+      auto content_length = std::to_string(DATA_SIZE);
+      std::string response_header = "HTTP/1.1 200 OK\r\n"
+                                    "Content-Length: " +
+                                    content_length +
+                                    "\r\n"
+                                    "Connection: close\r\n"
+                                    "\r\n";
+
+      ::send(cli,
+#ifdef _WIN32
+             static_cast<const char *>(response_header.c_str()),
+             static_cast<int>(response_header.size()),
+#else
+             response_header.c_str(), response_header.size(),
+#endif
+             0);
+
+      std::string chunk(64 * 1024, 'A');
+      size_t total_sent = 0;
+
+      while (total_sent < DATA_SIZE) {
+        auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
+        auto sent = ::send(cli,
+#ifdef _WIN32
+                           static_cast<const char *>(chunk.c_str()),
+                           static_cast<int>(to_send),
+#else
+                           chunk.c_str(), to_send,
+#endif
+                           0);
+        if (sent <= 0) break;
+        total_sent += static_cast<size_t>(sent);
+      }
+
+      detail::close_socket(cli);
+    }
+    detail::close_socket(srv);
+  });
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+  size_t total_received = 0;
+
+  {
+    Client cli("127.0.0.1", PORT + 2);
+    cli.set_read_timeout(10, 0);
+    cli.set_payload_max_length(EXPLICIT_LIMIT); // Explicit 150MB limit
+
+    auto res =
+        cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
+          total_received += data_length;
+          return true;
+        });
+
+    // Should fail because 200MB exceeds the explicit 150MB limit
+    EXPECT_FALSE(res);
+  }
+
+  server_thread.join();
+
+  EXPECT_LE(total_received, EXPLICIT_LIMIT)
+      << "Client with content_receiver should respect the explicit "
+      << "payload_max_length of " << EXPLICIT_LIMIT << " bytes, but read "
+      << total_received << " bytes.";
+}
+
+// Verify that an explicit set_payload_max_length larger than the response
+// allows the content_receiver to read all data successfully.
+TEST(ClientVulnerabilityTest,
+     ContentReceiverRespectsExplicitPayloadMaxLength250MB) {
+  static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
+  static constexpr size_t EXPLICIT_LIMIT = 250 * 1024 * 1024; // 250MB limit
+
+#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(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);
+
+      auto content_length = std::to_string(DATA_SIZE);
+      std::string response_header = "HTTP/1.1 200 OK\r\n"
+                                    "Content-Length: " +
+                                    content_length +
+                                    "\r\n"
+                                    "Connection: close\r\n"
+                                    "\r\n";
+
+      ::send(cli,
+#ifdef _WIN32
+             static_cast<const char *>(response_header.c_str()),
+             static_cast<int>(response_header.size()),
+#else
+             response_header.c_str(), response_header.size(),
+#endif
+             0);
+
+      std::string chunk(64 * 1024, 'A');
+      size_t total_sent = 0;
+
+      while (total_sent < DATA_SIZE) {
+        auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
+        auto sent = ::send(cli,
+#ifdef _WIN32
+                           static_cast<const char *>(chunk.c_str()),
+                           static_cast<int>(to_send),
+#else
+                           chunk.c_str(), to_send,
+#endif
+                           0);
+        if (sent <= 0) break;
+        total_sent += static_cast<size_t>(sent);
+      }
+
+#ifdef _WIN32
+      ::shutdown(cli, SD_SEND);
+#else
+      ::shutdown(cli, SHUT_WR);
+#endif
+      char drain[1024];
+      while (::recv(cli, drain, sizeof(drain), 0) > 0) {}
+      detail::close_socket(cli);
+    }
+    detail::close_socket(srv);
+  });
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+  size_t total_received = 0;
+
+  {
+    Client cli("127.0.0.1", PORT + 2);
+    cli.set_read_timeout(10, 0);
+    cli.set_payload_max_length(EXPLICIT_LIMIT); // Explicit 250MB limit
+
+    auto res =
+        cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
+          total_received += data_length;
+          return true;
+        });
+
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+  }
+
+  server_thread.join();
+
+  EXPECT_EQ(total_received, DATA_SIZE)
+      << "With explicit payload_max_length of " << EXPLICIT_LIMIT
+      << " bytes (larger than " << DATA_SIZE
+      << " bytes response), content_receiver should read all data, but only "
+         "read "
+      << total_received << " bytes.";
+}
+
 #if defined(CPPHTTPLIB_ZLIB_SUPPORT) && !defined(_WIN32)
 // Regression test for "zip bomb" attack on the client side: a malicious server
 // sends a small gzip-compressed response that decompresses to a huge payload.