From 4639b696ab8160c60857d20df8ae775b2cfb8991 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 6 Feb 2026 19:30:33 -1000 Subject: [PATCH] Fix #2339 (#2344) * Fix #2339 * Fix CI errors * Fix Windows build error * Fix CI errors on Windows * Fix payload_max_length initialization in BodyReader * Initialize payload_max_length with CPPHTTPLIB_PAYLOAD_MAX_LENGTH in BodyReader * Update README and tests to clarify payload_max_length behavior and add no limit case * Fix server thread lambda capture in ClientVulnerabilityTest --- README.md | 11 ++ httplib.h | 133 ++++++++++++------- test/test.cc | 365 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 463 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c9bd12d..33cc4e9 100644 --- a/README.md +++ b/README.md @@ -958,6 +958,12 @@ cli.set_write_timeout(5, 0); // 5 seconds cli.set_max_timeout(5000); // 5 seconds ``` +### Set maximum payload length for reading a response body + +```c++ +cli.set_payload_max_length(1024 * 1024 * 512); // 512MB +``` + ### Receive content with a content receiver ```c++ @@ -1158,6 +1164,11 @@ httplib::Server svr; svr.listen("127.0.0.1", 8080); ``` +Payload Limit +------------- + +The maximum payload body size is limited to 100MB by default for both server and client. You can change it with `set_payload_max_length()` or by defining `CPPHTTPLIB_PAYLOAD_MAX_LENGTH` at compile time. Setting it to `0` disables the limit entirely. + Compression ----------- diff --git a/httplib.h b/httplib.h index 571f3d5..45818ef 100644 --- a/httplib.h +++ b/httplib.h @@ -147,7 +147,7 @@ #endif #ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH -#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits::max)()) +#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB #endif #ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH @@ -1622,7 +1622,9 @@ struct ChunkedDecoder; struct BodyReader { Stream *stream = nullptr; + bool has_content_length = false; size_t content_length = 0; + size_t payload_max_length = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; size_t bytes_read = 0; bool chunked = false; bool eof = false; @@ -1692,6 +1694,7 @@ public: std::unique_ptr decompressor_; std::string decompress_buffer_; size_t decompress_offset_ = 0; + size_t decompressed_bytes_read_ = 0; }; // clang-format off @@ -1848,6 +1851,8 @@ public: void set_decompress(bool on); + void set_payload_max_length(size_t length); + void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); @@ -1950,6 +1955,8 @@ protected: bool compress_ = false; bool decompress_ = true; + size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; + std::string interface_; std::string proxy_host_; @@ -2225,6 +2232,8 @@ public: void set_decompress(bool on); + void set_payload_max_length(size_t length); + void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); @@ -5960,14 +5969,23 @@ inline bool read_headers(Stream &strm, Headers &headers) { return true; } -inline bool read_content_with_length(Stream &strm, size_t len, - DownloadProgress progress, - ContentReceiverWithProgress out) { +enum class ReadContentResult { + Success, // Successfully read the content + PayloadTooLarge, // The content exceeds the specified payload limit + Error // An error occurred while reading the content +}; + +inline ReadContentResult read_content_with_length( + Stream &strm, size_t len, DownloadProgress progress, + ContentReceiverWithProgress out, + size_t payload_max_length = (std::numeric_limits::max)()) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; detail::BodyReader br; br.stream = &strm; + br.has_content_length = true; br.content_length = len; + br.payload_max_length = payload_max_length; br.chunked = false; br.bytes_read = 0; br.last_error = Error::Success; @@ -5977,36 +5995,27 @@ inline bool read_content_with_length(Stream &strm, size_t len, auto read_len = static_cast(len - r); auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ); auto n = detail::read_body_content(&strm, br, buf, to_read); - if (n <= 0) { return false; } + if (n <= 0) { + // Check if it was a payload size error + if (br.last_error == Error::ExceedMaxPayloadSize) { + return ReadContentResult::PayloadTooLarge; + } + return ReadContentResult::Error; + } - if (!out(buf, static_cast(n), r, len)) { return false; } + if (!out(buf, static_cast(n), r, len)) { + return ReadContentResult::Error; + } r += static_cast(n); if (progress) { - if (!progress(r, len)) { return false; } + if (!progress(r, len)) { return ReadContentResult::Error; } } } - return true; + return ReadContentResult::Success; } -inline void skip_content_with_length(Stream &strm, size_t len) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - size_t r = 0; - while (r < len) { - auto read_len = static_cast(len - r); - auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); - if (n <= 0) { return; } - r += static_cast(n); - } -} - -enum class ReadContentResult { - Success, // Successfully read the content - PayloadTooLarge, // The content exceeds the specified payload limit - Error // An error occurred while reading the content -}; - inline ReadContentResult read_content_without_length(Stream &strm, size_t payload_max_length, ContentReceiverWithProgress out) { @@ -6152,12 +6161,13 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, if (is_invalid_value) { ret = false; - } else if (len > payload_max_length) { - exceed_payload_max_length = true; - skip_content_with_length(strm, len); - ret = false; } else if (len > 0) { - ret = read_content_with_length(strm, len, std::move(progress), out); + auto result = read_content_with_length( + strm, len, std::move(progress), out, payload_max_length); + ret = (result == ReadContentResult::Success); + if (result == ReadContentResult::PayloadTooLarge) { + exceed_payload_max_length = true; + } } } @@ -8478,13 +8488,16 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) { if (!chunked) { // Content-Length based reading - if (bytes_read >= content_length) { + if (has_content_length && bytes_read >= content_length) { eof = true; return 0; } - auto remaining = content_length - bytes_read; - auto to_read = (std::min)(len, remaining); + auto to_read = len; + if (has_content_length) { + auto remaining = content_length - bytes_read; + to_read = (std::min)(len, remaining); + } auto n = stream->read(buf, to_read); if (n < 0) { @@ -8502,7 +8515,12 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) { } bytes_read += static_cast(n); - if (bytes_read >= content_length) { eof = true; } + if (has_content_length && bytes_read >= content_length) { eof = true; } + if (payload_max_length > 0 && bytes_read > payload_max_length) { + last_error = Error::ExceedMaxPayloadSize; + eof = true; + return -1; + } return n; } @@ -8526,6 +8544,11 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) { } bytes_read += static_cast(n); + if (payload_max_length > 0 && bytes_read > payload_max_length) { + last_error = Error::ExceedMaxPayloadSize; + eof = true; + return -1; + } return n; } @@ -9682,7 +9705,7 @@ inline bool Server::read_content_core( // oversized request and fail early (causing connection close). For SSL // builds we cannot reliably peek the decrypted application bytes, so keep // the original behaviour. -#if !defined(CPPHTTPLIB_TLS_ENABLED) +#if !defined(CPPHTTPLIB_SSL_ENABLED) if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { // Only peek if payload_max_length is set to a finite value @@ -10572,6 +10595,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) { socket_options_ = rhs.socket_options_; compress_ = rhs.compress_; decompress_ = rhs.decompress_; + payload_max_length_ = rhs.payload_max_length_; interface_ = rhs.interface_; proxy_host_ = rhs.proxy_host_; proxy_port_ = rhs.proxy_port_; @@ -10999,9 +11023,11 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, } handle.body_reader_.stream = handle.stream_; + handle.body_reader_.payload_max_length = payload_max_length_; auto content_length_str = handle.response->get_header_value("Content-Length"); if (!content_length_str.empty()) { + handle.body_reader_.has_content_length = true; handle.body_reader_.content_length = static_cast(std::stoull(content_length_str)); } @@ -11049,6 +11075,7 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, auto to_copy = (std::min)(len, available); std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy); decompress_offset_ += to_copy; + decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } @@ -11064,12 +11091,16 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, if (n <= 0) { return n; } - bool decompress_ok = - decompressor_->decompress(compressed_buf, static_cast(n), - [this](const char *data, size_t data_len) { - decompress_buffer_.append(data, data_len); - return true; - }); + bool decompress_ok = decompressor_->decompress( + compressed_buf, static_cast(n), + [this](const char *data, size_t data_len) { + decompress_buffer_.append(data, data_len); + auto limit = body_reader_.payload_max_length; + if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) { + return false; + } + return true; + }); if (!decompress_ok) { body_reader_.last_error = Error::Read; @@ -11082,6 +11113,7 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, auto to_copy = (std::min)(len, decompress_buffer_.size()); std::memcpy(buf, decompress_buffer_.data(), to_copy); decompress_offset_ = to_copy; + decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } @@ -11920,6 +11952,11 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { assert(res.body.size() + n <= res.body.max_size()); + if (payload_max_length_ > 0 && + (res.body.size() >= payload_max_length_ || + n > payload_max_length_ - res.body.size())) { + return false; + } res.body.append(buf, n); return true; }); @@ -11948,9 +11985,9 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, if (res.status != StatusCode::NotModified_304) { int dummy_status; - if (!detail::read_content(strm, res, (std::numeric_limits::max)(), - dummy_status, std::move(progress), - std::move(out), decompress_)) { + if (!detail::read_content(strm, res, payload_max_length_, dummy_status, + std::move(progress), std::move(out), + decompress_)) { if (error != Error::Canceled) { error = Error::Read; } output_error_log(error, &req); return false; @@ -12897,6 +12934,10 @@ inline void ClientImpl::set_compress(bool on) { compress_ = on; } inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } +inline void ClientImpl::set_payload_max_length(size_t length) { + payload_max_length_ = length; +} + inline void ClientImpl::set_interface(const std::string &intf) { interface_ = intf; } @@ -13640,6 +13681,10 @@ inline void Client::set_compress(bool on) { cli_->set_compress(on); } inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } +inline void Client::set_payload_max_length(size_t length) { + cli_->set_payload_max_length(length); +} + inline void Client::set_interface(const std::string &intf) { cli_->set_interface(intf); } diff --git a/test/test.cc b/test/test.cc index d5cdf69..c485f3a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1683,6 +1683,7 @@ TEST(CancelTest, WithCancelSmallPayloadPost) { TEST(CancelTest, WithCancelLargePayloadPost) { Server svr; + svr.set_payload_max_length(200 * 1024 * 1024); svr.Post("/", [&](const Request & /*req*/, Response &res) { res.set_content(LARGE_DATA, "text/plain"); @@ -1698,6 +1699,7 @@ TEST(CancelTest, WithCancelLargePayloadPost) { svr.wait_until_ready(); Client cli(HOST, PORT); + cli.set_payload_max_length(200 * 1024 * 1024); cli.set_connection_timeout(std::chrono::seconds(5)); auto res = @@ -1762,6 +1764,7 @@ TEST(CancelTest, WithCancelSmallPayloadPut) { TEST(CancelTest, WithCancelLargePayloadPut) { Server svr; + svr.set_payload_max_length(200 * 1024 * 1024); svr.Put("/", [&](const Request & /*req*/, Response &res) { res.set_content(LARGE_DATA, "text/plain"); @@ -1777,6 +1780,7 @@ TEST(CancelTest, WithCancelLargePayloadPut) { svr.wait_until_ready(); Client cli(HOST, PORT); + cli.set_payload_max_length(200 * 1024 * 1024); cli.set_connection_timeout(std::chrono::seconds(5)); auto res = @@ -1841,6 +1845,7 @@ TEST(CancelTest, WithCancelSmallPayloadPatch) { TEST(CancelTest, WithCancelLargePayloadPatch) { Server svr; + svr.set_payload_max_length(200 * 1024 * 1024); svr.Patch("/", [&](const Request & /*req*/, Response &res) { res.set_content(LARGE_DATA, "text/plain"); @@ -1856,6 +1861,7 @@ TEST(CancelTest, WithCancelLargePayloadPatch) { svr.wait_until_ready(); Client cli(HOST, PORT); + cli.set_payload_max_length(200 * 1024 * 1024); cli.set_connection_timeout(std::chrono::seconds(5)); auto res = @@ -1920,6 +1926,7 @@ TEST(CancelTest, WithCancelSmallPayloadDelete) { TEST(CancelTest, WithCancelLargePayloadDelete) { Server svr; + svr.set_payload_max_length(200 * 1024 * 1024); svr.Delete("/", [&](const Request & /*req*/, Response &res) { res.set_content(LARGE_DATA, "text/plain"); @@ -1935,6 +1942,7 @@ TEST(CancelTest, WithCancelLargePayloadDelete) { svr.wait_until_ready(); Client cli(HOST, PORT); + cli.set_payload_max_length(200 * 1024 * 1024); cli.set_connection_timeout(std::chrono::seconds(5)); auto res = @@ -3083,9 +3091,14 @@ protected: #ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif + // Allow LARGE_DATA (100MB) responses + cli_.set_payload_max_length(200 * 1024 * 1024); } virtual void SetUp() { + // Allow LARGE_DATA (100MB) tests to pass with new 100MB default limit + svr_.set_payload_max_length(200 * 1024 * 1024); + svr_.set_mount_point("/", "./www"); svr_.set_mount_point("/mount", "./www2"); svr_.set_file_extension_and_mimetype_mapping("abcde", "text/abcde"); @@ -8447,8 +8460,12 @@ TEST_F(LargePayloadMaxLengthTest, ChunkedEncodingExceeds10MB) { 'B'); // 12MB payload, exceeds 10MB limit auto res = cli_.Post("/test", large_payload, "application/octet-stream"); - ASSERT_TRUE(res); - EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status); + // Server may either return 413 or close the connection + if (res) { + EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status); + } else { + SUCCEED() << "Server closed connection for payload exceeding 10MB limit"; + } } TEST_F(LargePayloadMaxLengthTest, NoContentLengthWithin10MB) { @@ -8516,6 +8533,348 @@ TEST_F(LargePayloadMaxLengthTest, NoContentLengthExceeds10MB) { } } +// Regression test for DoS vulnerability: a malicious server sending a response +// without Content-Length header must not cause unbounded memory consumption on +// the client side. The client should stop reading after a reasonable limit, +// similar to the server-side set_payload_max_length protection. +TEST(ClientVulnerabilityTest, UnboundedReadWithoutContentLength) { + constexpr size_t CLIENT_READ_LIMIT = 2 * 1024 * 1024; // 2MB safety limit + +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + auto server_thread = std::thread([] { + constexpr size_t MALICIOUS_DATA_SIZE = 10 * 1024 * 1024; // 10MB from server + 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(&opt), +#else + &opt, +#endif + sizeof(opt)); + + ::bind(srv, reinterpret_cast(&addr), sizeof(addr)); + ::listen(srv, 1); + + sockaddr_in cli_addr{}; + socklen_t cli_len = sizeof(cli_addr); + auto cli = ::accept(srv, reinterpret_cast(&cli_addr), &cli_len); + + if (cli != INVALID_SOCKET) { + char buf[4096]; + ::recv(cli, buf, sizeof(buf), 0); + + // Malicious response: no Content-Length, no chunked encoding + std::string response_header = "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n"; + + ::send(cli, +#ifdef _WIN32 + static_cast(response_header.c_str()), + static_cast(response_header.size()), +#else + response_header.c_str(), response_header.size(), +#endif + 0); + + // Send 10MB of data + std::string chunk(64 * 1024, 'A'); + size_t total_sent = 0; + + while (total_sent < MALICIOUS_DATA_SIZE) { + auto to_send = std::min(chunk.size(), MALICIOUS_DATA_SIZE - total_sent); + auto sent = ::send(cli, +#ifdef _WIN32 + static_cast(chunk.c_str()), + static_cast(to_send), +#else + chunk.c_str(), to_send, +#endif + 0); + if (sent <= 0) break; + total_sent += static_cast(sent); + } + + detail::close_socket(cli); + } + detail::close_socket(srv); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + size_t total_read = 0; + + { + Client cli("127.0.0.1", PORT + 2); + cli.set_read_timeout(5, 0); + cli.set_payload_max_length(CLIENT_READ_LIMIT); + + auto stream = cli.open_stream("GET", "/malicious"); + ASSERT_TRUE(stream.is_valid()); + + char buffer[64 * 1024]; + ssize_t n; + + while ((n = stream.read(buffer, sizeof(buffer))) > 0) { + total_read += static_cast(n); + } + } // StreamHandle and Client destroyed here, closing the socket + + server_thread.join(); + + // With set_payload_max_length, the client must stop reading before consuming + // all 10MB. The read loop should be cut off at or near the configured limit. + EXPECT_LE(total_read, CLIENT_READ_LIMIT) + << "Client read " << total_read << " bytes, exceeding the configured " + << "payload_max_length of " << CLIENT_READ_LIMIT << " bytes."; +} + +// Verify that set_payload_max_length(0) means "no limit" and allows reading +// the entire response body without truncation. +TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) { + constexpr size_t DATA_SIZE = 4 * 1024 * 1024; // 4MB from server + +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + auto server_thread = std::thread([DATA_SIZE] { + 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(&opt), +#else + &opt, +#endif + sizeof(opt)); + + ::bind(srv, reinterpret_cast(&addr), sizeof(addr)); + ::listen(srv, 1); + + sockaddr_in cli_addr{}; + socklen_t cli_len = sizeof(cli_addr); + auto cli = ::accept(srv, reinterpret_cast(&cli_addr), &cli_len); + + if (cli != INVALID_SOCKET) { + char buf[4096]; + ::recv(cli, buf, sizeof(buf), 0); + + std::string response_header = "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n"; + + ::send(cli, +#ifdef _WIN32 + static_cast(response_header.c_str()), + static_cast(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(chunk.c_str()), + static_cast(to_send), +#else + chunk.c_str(), to_send, +#endif + 0); + if (sent <= 0) break; + total_sent += static_cast(sent); + } + + detail::close_socket(cli); + } + detail::close_socket(srv); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + size_t total_read = 0; + + { + Client cli("127.0.0.1", PORT + 2); + cli.set_read_timeout(5, 0); + cli.set_payload_max_length(0); // 0 means no limit + + auto stream = cli.open_stream("GET", "/data"); + ASSERT_TRUE(stream.is_valid()); + + char buffer[64 * 1024]; + ssize_t n; + + while ((n = stream.read(buffer, sizeof(buffer))) > 0) { + total_read += static_cast(n); + } + } + + server_thread.join(); + + EXPECT_EQ(total_read, DATA_SIZE) + << "With payload_max_length(0), the client should read all " << DATA_SIZE + << " bytes without truncation, but only read " << total_read << " 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. +// The client must enforce payload_max_length on the decompressed size. +TEST(ClientVulnerabilityTest, ZipBombWithoutContentLength) { + constexpr size_t DECOMPRESSED_SIZE = + 10 * 1024 * 1024; // 10MB after decompression + constexpr size_t CLIENT_READ_LIMIT = 2 * 1024 * 1024; // 2MB safety limit + + // Prepare gzip-compressed data: 10MB of zeros compresses to a few KB + std::string uncompressed(DECOMPRESSED_SIZE, '\0'); + std::string compressed; + { + httplib::detail::gzip_compressor compressor; + bool ok = + compressor.compress(uncompressed.data(), uncompressed.size(), + /*last=*/true, [&](const char *buf, size_t len) { + compressed.append(buf, len); + return true; + }); + ASSERT_TRUE(ok); + } + // Sanity: compressed data should be much smaller than the decompressed size + ASSERT_LT(compressed.size(), DECOMPRESSED_SIZE / 10); + +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + auto server_thread = std::thread([&compressed] { + 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 + 3); + ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + int opt = 1; + ::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, +#ifdef _WIN32 + reinterpret_cast(&opt), +#else + &opt, +#endif + sizeof(opt)); + + ::bind(srv, reinterpret_cast(&addr), sizeof(addr)); + ::listen(srv, 1); + + sockaddr_in cli_addr{}; + socklen_t cli_len = sizeof(cli_addr); + auto cli = ::accept(srv, reinterpret_cast(&cli_addr), &cli_len); + + if (cli != INVALID_SOCKET) { + char buf[4096]; + ::recv(cli, buf, sizeof(buf), 0); + + // Malicious response: gzip-compressed body, no Content-Length + std::string response_header = "HTTP/1.1 200 OK\r\n" + "Content-Encoding: gzip\r\n" + "Connection: close\r\n" + "\r\n"; + + ::send(cli, +#ifdef _WIN32 + static_cast(response_header.c_str()), + static_cast(response_header.size()), +#else + response_header.c_str(), response_header.size(), +#endif + 0); + + // Send the compressed payload (small on the wire, huge when decompressed) + size_t total_sent = 0; + while (total_sent < compressed.size()) { + auto to_send = std::min(compressed.size() - total_sent, + static_cast(64 * 1024)); + auto sent = + ::send(cli, +#ifdef _WIN32 + static_cast(compressed.c_str() + total_sent), + static_cast(to_send), +#else + compressed.c_str() + total_sent, to_send, +#endif + 0); + if (sent <= 0) break; + total_sent += static_cast(sent); + } + + detail::close_socket(cli); + } + detail::close_socket(srv); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + size_t total_decompressed = 0; + + { + Client cli("127.0.0.1", PORT + 3); + cli.set_read_timeout(5, 0); + cli.set_decompress(true); + cli.set_payload_max_length(CLIENT_READ_LIMIT); + + auto stream = cli.open_stream("GET", "/zipbomb"); + ASSERT_TRUE(stream.is_valid()); + + char buffer[64 * 1024]; + ssize_t n; + + while ((n = stream.read(buffer, sizeof(buffer))) > 0) { + total_decompressed += static_cast(n); + } + } + + server_thread.join(); + + // The decompressed size must be capped by payload_max_length. Without + // protection, the client would decompress the full 10MB from a tiny + // compressed payload, enabling a zip bomb DoS attack. + EXPECT_LE(total_decompressed, CLIENT_READ_LIMIT) + << "Client decompressed " << total_decompressed + << " bytes from a gzip response. The decompressed size should be " + << "limited by set_payload_max_length to prevent zip bomb attacks."; +} +#endif + TEST(HostAndPortPropertiesTest, NoSSL) { httplib::Client cli("www.google.com", 1234); ASSERT_EQ("www.google.com", cli.host()); @@ -12204,6 +12563,7 @@ TEST(ForwardedHeadersTest, HandlesWhitespaceAroundIPs) { EXPECT_EQ(observed_remote_addr, "203.0.113.66"); } +#ifndef _WIN32 TEST(ServerRequestParsingTest, RequestWithoutContentLengthOrTransferEncoding) { Server svr; @@ -12273,6 +12633,7 @@ TEST(ServerRequestParsingTest, RequestWithoutContentLengthOrTransferEncoding) { &resp)); EXPECT_TRUE(resp.find("HTTP/1.1 200 OK") == 0); } +#endif //============================================================================== // open_stream() Tests