From 2d2efe46dab0385c0fcfe273ee96efcc49b02f3e Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 1 May 2026 21:28:57 +0900 Subject: [PATCH] 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. --- httplib.h | 10 ++- ...e-minimized-client_fuzzer-5188033728282624 | 3 + test/test.cc | 75 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 test/fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-5188033728282624 diff --git a/httplib.h b/httplib.h index 14a71e5..fe3b716 100644 --- a/httplib.h +++ b/httplib.h @@ -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(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(len); + if (payload_max_length_ > 0 && reserve_len > payload_max_length_) { + reserve_len = payload_max_length_; + } + res.body.reserve(reserve_len); } } diff --git a/test/fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-5188033728282624 b/test/fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-5188033728282624 new file mode 100644 index 0000000..4f6f3ac --- /dev/null +++ b/test/fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-5188033728282624 @@ -0,0 +1,3 @@ + HTTP/1.1 777 +Content-Length:20000000000 + diff --git a/test/test.cc b/test/test.cc index 4a78198..f90a024 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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(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: 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(response.c_str()), + static_cast(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.