From e41ec36274a235d8b0bbf21d57e32068a30f6519 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 8 Mar 2026 22:30:20 -0400 Subject: [PATCH] Fix handling of malformed Content-Length in open_stream and add tests --- httplib.h | 14 +++++--- test/test.cc | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index 7070416d..53aa2ba7 100644 --- a/httplib.h +++ b/httplib.h @@ -12414,11 +12414,17 @@ 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()) { + if (handle.response->has_header("Content-Length")) { + bool is_invalid = false; + auto content_length = detail::get_header_value_u64( + handle.response->headers, "Content-Length", 0, 0, is_invalid); + if (is_invalid) { + handle.error = Error::Read; + handle.response.reset(); + return handle; + } handle.body_reader_.has_content_length = true; - handle.body_reader_.content_length = - static_cast(std::stoull(content_length_str)); + handle.body_reader_.content_length = content_length; } auto transfer_encoding = diff --git a/test/test.cc b/test/test.cc index 0c8c39ac..3f9dfcad 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13712,6 +13712,102 @@ TEST_F(OpenStreamTest, ProhibitedTrailersAreIgnored_Stream) { EXPECT_EQ(std::string(""), handle.response->get_header_value("X-Allowed")); } +static std::thread serve_single_response(int port, + const std::string &response) { + return std::thread([port, response] { + 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)); + ::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); + + ::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); + }); +} + +TEST(OpenStreamMalformedContentLength, InvalidArgument) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + auto server_thread = + serve_single_response(PORT + 2, "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: not-a-number\r\n" + "Connection: close\r\n" + "\r\n" + "hello"); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + Client cli("127.0.0.1", PORT + 2); + auto handle = cli.open_stream("GET", "/"); + EXPECT_FALSE(handle.is_valid()); + + server_thread.join(); +} + +TEST(OpenStreamMalformedContentLength, OutOfRange) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif + + auto server_thread = serve_single_response( + PORT + 2, "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 99999999999999999999999999\r\n" + "Connection: close\r\n" + "\r\n" + "hello"); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Before the fix, std::stoull would throw std::out_of_range here and + // crash the process. After the fix, strtoull silently clamps to + // ULLONG_MAX so the stream opens without crashing. The important thing + // is that the process does NOT terminate. + Client cli("127.0.0.1", PORT + 2); + auto handle = cli.open_stream("GET", "/"); + EXPECT_TRUE(handle.is_valid()); + + server_thread.join(); +} + #ifdef CPPHTTPLIB_ZLIB_SUPPORT TEST_F(OpenStreamTest, Gzip) { Client cli("127.0.0.1", port_);