mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
* 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
This commit is contained in:
@@ -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
|
||||
-----------
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH
|
||||
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits<size_t>::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<detail::decompressor> 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<size_t>::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<size_t>(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<size_t>(n), r, len)) { return false; }
|
||||
if (!out(buf, static_cast<size_t>(n), r, len)) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
r += static_cast<size_t>(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<size_t>(len - r);
|
||||
auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ));
|
||||
if (n <= 0) { return; }
|
||||
r += static_cast<size_t>(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<size_t>(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<size_t>(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<size_t>(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<ssize_t>(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<size_t>(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<size_t>(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<ssize_t>(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<size_t>::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);
|
||||
}
|
||||
|
||||
+363
-2
@@ -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<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: 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<const char *>(response_header.c_str()),
|
||||
static_cast<int>(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<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_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<size_t>(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<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);
|
||||
|
||||
std::string response_header = "HTTP/1.1 200 OK\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_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<size_t>(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<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: 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<const char *>(response_header.c_str()),
|
||||
static_cast<int>(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<size_t>(64 * 1024));
|
||||
auto sent =
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(compressed.c_str() + total_sent),
|
||||
static_cast<int>(to_send),
|
||||
#else
|
||||
compressed.c_str() + total_sent, 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_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<size_t>(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
|
||||
|
||||
Reference in New Issue
Block a user