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);
|
output_error_log(error, &req);
|
||||||
return false;
|
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.";
|
<< " 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,
|
// Verify that content_receiver bypasses the default payload_max_length,
|
||||||
// allowing streaming downloads larger than 100MB without requiring an explicit
|
// allowing streaming downloads larger than 100MB without requiring an explicit
|
||||||
// set_payload_max_length call.
|
// set_payload_max_length call.
|
||||||
|
|||||||
Reference in New Issue
Block a user