mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
HTTP/1.1 777
|
||||
Content-Length:20000000000
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user