diff --git a/CVE-2025-53629-pre.patch b/CVE-2025-53629-pre.patch new file mode 100644 index 0000000000000000000000000000000000000000..db4e0363995efccc762dd3056f1f0ad66591a052 --- /dev/null +++ b/CVE-2025-53629-pre.patch @@ -0,0 +1,442 @@ +From 082acacd4581d10e05fccbe9cb336aa7822c4ea2 Mon Sep 17 00:00:00 2001 +From: yhirose +Date: Tue, 8 Jul 2025 17:11:13 -0400 +Subject: [PATCH] Merge commit from fork + +* Fix Persistency of Unbounded Memory Allocation in Chunked/No-Length Requests Vulnerability + +* Revert HTTP status code from 413 to 400 +--- + httplib.h | 92 ++++++++++++++----- + test/test.cc | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 325 insertions(+), 22 deletions(-) + +diff --git a/httplib.h b/httplib.h +index eaea3d85ae..63950d9afa 100644 +--- a/httplib.h ++++ b/httplib.h +@@ -4642,52 +4642,79 @@ inline void skip_content_with_length(Stream &strm, uint64_t len) { + } + } + +-inline bool read_content_without_length(Stream &strm, +- 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_without_length(Stream &strm, size_t payload_max_length, ++ ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + uint64_t r = 0; + for (;;) { + auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); +- if (n == 0) { return true; } +- if (n < 0) { return false; } ++ if (n == 0) { return ReadContentResult::Success; } ++ if (n < 0) { return ReadContentResult::Error; } ++ ++ // Check if adding this data would exceed the payload limit ++ if (r > payload_max_length || ++ payload_max_length - r < static_cast(n)) { ++ return ReadContentResult::PayloadTooLarge; ++ } + +- if (!out(buf, static_cast(n), r, 0)) { return false; } ++ if (!out(buf, static_cast(n), r, 0)) { ++ return ReadContentResult::Error; ++ } + r += static_cast(n); + } + +- return true; ++ return ReadContentResult::Success; + } + + template +-inline bool read_content_chunked(Stream &strm, T &x, +- ContentReceiverWithProgress out) { ++inline ReadContentResult read_content_chunked(Stream &strm, T &x, ++ size_t payload_max_length, ++ ContentReceiverWithProgress out) { + const auto bufsiz = 16; + char buf[bufsiz]; + + stream_line_reader line_reader(strm, buf, bufsiz); + +- if (!line_reader.getline()) { return false; } ++ if (!line_reader.getline()) { return ReadContentResult::Error; } + + unsigned long chunk_len; ++ uint64_t total_len = 0; + while (true) { + char *end_ptr; + + chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16); + +- if (end_ptr == line_reader.ptr()) { return false; } +- if (chunk_len == ULONG_MAX) { return false; } ++ if (end_ptr == line_reader.ptr()) { return ReadContentResult::Error; } ++ if (chunk_len == ULONG_MAX) { return ReadContentResult::Error; } + + if (chunk_len == 0) { break; } + ++ // Check if adding this chunk would exceed the payload limit ++ if (total_len > payload_max_length || ++ payload_max_length - total_len < chunk_len) { ++ return ReadContentResult::PayloadTooLarge; ++ } ++ ++ total_len += chunk_len; ++ + if (!read_content_with_length(strm, chunk_len, nullptr, out)) { +- return false; ++ return ReadContentResult::Error; + } + +- if (!line_reader.getline()) { return false; } ++ if (!line_reader.getline()) { return ReadContentResult::Error; } + +- if (strcmp(line_reader.ptr(), "\r\n") != 0) { return false; } ++ if (strcmp(line_reader.ptr(), "\r\n") != 0) { ++ return ReadContentResult::Error; ++ } + +- if (!line_reader.getline()) { return false; } ++ if (!line_reader.getline()) { return ReadContentResult::Error; } + } + + assert(chunk_len == 0); +@@ -4704,14 +4731,18 @@ inline bool read_content_chunked(Stream &strm, T &x, + // + // According to the reference code in RFC 9112, cpp-httplib now allows + // chunked transfer coding data without the final CRLF. +- if (!line_reader.getline()) { return true; } ++ if (!line_reader.getline()) { return ReadContentResult::Success; } + + size_t trailer_header_count = 0; + while (strcmp(line_reader.ptr(), "\r\n") != 0) { +- if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } ++ if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { ++ return ReadContentResult::Error; ++ } + + // Check trailer header count limit +- if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } ++ if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { ++ return ReadContentResult::Error; ++ } + + // Exclude line terminator + constexpr auto line_terminator_len = 2; +@@ -4724,10 +4755,10 @@ inline bool read_content_chunked(Stream &strm, T &x, + + trailer_header_count++; + +- if (!line_reader.getline()) { return false; } ++ if (!line_reader.getline()) { return ReadContentResult::Error; } + } + +- return true; ++ return ReadContentResult::Success; + } + + inline bool is_chunked_transfer_encoding(const Headers &headers) { +@@ -4801,9 +4832,26 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, + auto exceed_payload_max_length = false; + + if (is_chunked_transfer_encoding(x.headers)) { +- ret = read_content_chunked(strm, x, out); ++ auto result = read_content_chunked(strm, x, payload_max_length, out); ++ if (result == ReadContentResult::Success) { ++ ret = true; ++ } else if (result == ReadContentResult::PayloadTooLarge) { ++ exceed_payload_max_length = true; ++ ret = false; ++ } else { ++ ret = false; ++ } + } else if (!has_header(x.headers, "Content-Length")) { +- ret = read_content_without_length(strm, out); ++ auto result = ++ read_content_without_length(strm, payload_max_length, out); ++ if (result == ReadContentResult::Success) { ++ ret = true; ++ } else if (result == ReadContentResult::PayloadTooLarge) { ++ exceed_payload_max_length = true; ++ ret = false; ++ } else { ++ ret = false; ++ } + } else { + auto is_invalid_value = false; + auto len = get_header_value_u64( +diff --git a/test/test.cc b/test/test.cc +index 8e8299b76f..023e92602f 100644 +--- a/test/test.cc ++++ b/test/test.cc +@@ -7763,6 +7763,261 @@ TEST_F(PayloadMaxLengthTest, ExceedLimit) { + EXPECT_EQ(StatusCode::OK_200, res->status); + } + ++TEST_F(PayloadMaxLengthTest, ChunkedEncodingSecurityTest) { ++ // Test chunked encoding with payload exceeding the 8-byte limit ++ std::string large_chunked_data(16, 'A'); // 16 bytes, exceeds 8-byte limit ++ ++ auto res = cli_.Post("/test", large_chunked_data, "text/plain"); ++ ASSERT_TRUE(res); ++ EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status); ++} ++ ++TEST_F(PayloadMaxLengthTest, ChunkedEncodingWithinLimit) { ++ // Test chunked encoding with payload within the 8-byte limit ++ std::string small_chunked_data(4, 'B'); // 4 bytes, within 8-byte limit ++ ++ auto res = cli_.Post("/test", small_chunked_data, "text/plain"); ++ ASSERT_TRUE(res); ++ EXPECT_EQ(StatusCode::OK_200, res->status); ++} ++ ++TEST_F(PayloadMaxLengthTest, RawSocketChunkedTest) { ++ // Test using send_request to send chunked data exceeding payload limit ++ std::string chunked_request = "POST /test HTTP/1.1\r\n" ++ "Host: " + ++ std::string(HOST) + ":" + std::to_string(PORT) + ++ "\r\n" ++ "Transfer-Encoding: chunked\r\n" ++ "Connection: close\r\n" ++ "\r\n" ++ "a\r\n" // 10 bytes chunk (exceeds 8-byte limit) ++ "0123456789\r\n" ++ "0\r\n" // End chunk ++ "\r\n"; ++ ++ std::string response; ++ bool result = send_request(1, chunked_request, &response); ++ ++ if (!result) { ++ // If send_request fails, it might be because the server closed the ++ // connection due to payload limit enforcement, which is acceptable ++ SUCCEED() ++ << "Server rejected oversized chunked request (connection closed)"; ++ } else { ++ // If we got a response, check if it's an error response or connection was ++ // closed early Short response length indicates connection was closed due to ++ // payload limit ++ if (response.length() <= 10) { ++ SUCCEED() << "Server closed connection for oversized chunked request"; ++ } else { ++ // Check for error status codes ++ EXPECT_TRUE(response.find("413") != std::string::npos || ++ response.find("Payload Too Large") != std::string::npos || ++ response.find("400") != std::string::npos); ++ } ++ } ++} ++ ++TEST_F(PayloadMaxLengthTest, NoContentLengthPayloadLimit) { ++ // Test request without Content-Length header exceeding payload limit ++ std::string request_without_content_length = "POST /test HTTP/1.1\r\n" ++ "Host: " + ++ std::string(HOST) + ":" + ++ std::to_string(PORT) + ++ "\r\n" ++ "Connection: close\r\n" ++ "\r\n"; ++ ++ // Add payload exceeding the 8-byte limit ++ std::string large_payload(16, 'X'); // 16 bytes, exceeds 8-byte limit ++ request_without_content_length += large_payload; ++ ++ std::string response; ++ bool result = send_request(1, request_without_content_length, &response); ++ ++ if (!result) { ++ // If send_request fails, server likely closed connection due to payload ++ // limit ++ SUCCEED() << "Server rejected oversized request without Content-Length " ++ "(connection closed)"; ++ } else { ++ // Check if server responded with error or closed connection early ++ if (response.length() <= 10) { ++ SUCCEED() << "Server closed connection for oversized request without " ++ "Content-Length"; ++ } else { ++ // Check for error status codes ++ EXPECT_TRUE(response.find("413") != std::string::npos || ++ response.find("Payload Too Large") != std::string::npos || ++ response.find("400") != std::string::npos); ++ } ++ } ++} ++ ++TEST_F(PayloadMaxLengthTest, NoContentLengthWithinLimit) { ++ // Test request without Content-Length header within payload limit ++ std::string request_without_content_length = "POST /test HTTP/1.1\r\n" ++ "Host: " + ++ std::string(HOST) + ":" + ++ std::to_string(PORT) + ++ "\r\n" ++ "Connection: close\r\n" ++ "\r\n"; ++ ++ // Add payload within the 8-byte limit ++ std::string small_payload(4, 'Y'); // 4 bytes, within 8-byte limit ++ request_without_content_length += small_payload; ++ ++ std::string response; ++ bool result = send_request(1, request_without_content_length, &response); ++ ++ // For requests without Content-Length, the server may have different behavior ++ // The key is that it should not reject due to payload limit for small ++ // payloads ++ if (result) { ++ // Check for any HTTP response (success or error, but not connection closed) ++ if (response.length() > 10) { ++ SUCCEED() ++ << "Server processed request without Content-Length within limit"; ++ } else { ++ // Short response might indicate connection closed, which is acceptable ++ SUCCEED() << "Server closed connection for request without " ++ "Content-Length (acceptable behavior)"; ++ } ++ } else { ++ // Connection failure might be due to protocol requirements ++ SUCCEED() << "Connection issue with request without Content-Length " ++ "(environment-specific)"; ++ } ++} ++ ++class LargePayloadMaxLengthTest : public ::testing::Test { ++protected: ++ LargePayloadMaxLengthTest() ++ : cli_(HOST, PORT) ++#ifdef CPPHTTPLIB_OPENSSL_SUPPORT ++ , ++ svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) ++#endif ++ { ++#ifdef CPPHTTPLIB_OPENSSL_SUPPORT ++ cli_.enable_server_certificate_verification(false); ++#endif ++ } ++ ++ virtual void SetUp() { ++ // Set 10MB payload limit ++ const size_t LARGE_PAYLOAD_LIMIT = 10 * 1024 * 1024; // 10MB ++ svr_.set_payload_max_length(LARGE_PAYLOAD_LIMIT); ++ ++ svr_.Post("/test", [&](const Request & /*req*/, Response &res) { ++ res.set_content("Large payload test", "text/plain"); ++ }); ++ ++ t_ = thread([&]() { ASSERT_TRUE(svr_.listen(HOST, PORT)); }); ++ svr_.wait_until_ready(); ++ } ++ ++ virtual void TearDown() { ++ svr_.stop(); ++ t_.join(); ++ } ++ ++#ifdef CPPHTTPLIB_OPENSSL_SUPPORT ++ SSLClient cli_; ++ SSLServer svr_; ++#else ++ Client cli_; ++ Server svr_; ++#endif ++ thread t_; ++}; ++ ++TEST_F(LargePayloadMaxLengthTest, ChunkedEncodingWithin10MB) { ++ // Test chunked encoding with payload within 10MB limit ++ std::string medium_payload(5 * 1024 * 1024, ++ 'A'); // 5MB payload, within 10MB limit ++ ++ auto res = cli_.Post("/test", medium_payload, "application/octet-stream"); ++ ASSERT_TRUE(res); ++ EXPECT_EQ(StatusCode::OK_200, res->status); ++} ++ ++TEST_F(LargePayloadMaxLengthTest, ChunkedEncodingExceeds10MB) { ++ // Test chunked encoding with payload exceeding 10MB limit ++ std::string large_payload(12 * 1024 * 1024, ++ '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); ++} ++ ++TEST_F(LargePayloadMaxLengthTest, NoContentLengthWithin10MB) { ++ // Test request without Content-Length header within 10MB limit ++ std::string request_without_content_length = "POST /test HTTP/1.1\r\n" ++ "Host: " + ++ std::string(HOST) + ":" + ++ std::to_string(PORT) + ++ "\r\n" ++ "Connection: close\r\n" ++ "\r\n"; ++ ++ // Add 1MB payload (within 10MB limit) ++ std::string medium_payload(1024 * 1024, 'C'); // 1MB payload ++ request_without_content_length += medium_payload; ++ ++ std::string response; ++ bool result = send_request(5, request_without_content_length, &response); ++ ++ if (result) { ++ // Should get a proper HTTP response for payloads within limit ++ if (response.length() > 10) { ++ SUCCEED() << "Server processed 1MB request without Content-Length within " ++ "10MB limit"; ++ } else { ++ SUCCEED() << "Server closed connection (acceptable behavior for no " ++ "Content-Length)"; ++ } ++ } else { ++ SUCCEED() << "Connection issue with 1MB payload (environment-specific)"; ++ } ++} ++ ++TEST_F(LargePayloadMaxLengthTest, NoContentLengthExceeds10MB) { ++ // Test request without Content-Length header exceeding 10MB limit ++ std::string request_without_content_length = "POST /test HTTP/1.1\r\n" ++ "Host: " + ++ std::string(HOST) + ":" + ++ std::to_string(PORT) + ++ "\r\n" ++ "Connection: close\r\n" ++ "\r\n"; ++ ++ // Add 12MB payload (exceeds 10MB limit) ++ std::string large_payload(12 * 1024 * 1024, 'D'); // 12MB payload ++ request_without_content_length += large_payload; ++ ++ std::string response; ++ bool result = send_request(10, request_without_content_length, &response); ++ ++ if (!result) { ++ // Server should close connection due to payload limit ++ SUCCEED() << "Server rejected 12MB request without Content-Length " ++ "(connection closed)"; ++ } else { ++ // Check for error response ++ if (response.length() <= 10) { ++ SUCCEED() ++ << "Server closed connection for 12MB request exceeding 10MB limit"; ++ } else { ++ EXPECT_TRUE(response.find("413") != std::string::npos || ++ response.find("Payload Too Large") != std::string::npos || ++ response.find("400") != std::string::npos); ++ } ++ } ++} ++ + TEST(HostAndPortPropertiesTest, NoSSL) { + httplib::Client cli("www.google.com", 1234); + ASSERT_EQ("www.google.com", cli.host()); diff --git a/CVE-2025-53629.patch b/CVE-2025-53629.patch new file mode 100644 index 0000000000000000000000000000000000000000..a09147823f03b060762f64b12dd4e6382f10fae1 --- /dev/null +++ b/CVE-2025-53629.patch @@ -0,0 +1,258 @@ +From 17ba303889b8d4d719be3879a70639ab653efb99 Mon Sep 17 00:00:00 2001 +From: yhirose +Date: Wed, 9 Jul 2025 07:10:09 -0400 +Subject: [PATCH] Merge commit from fork + +* Fix HTTP Header Smuggling due to insecure trailers merge + +* Improve performance +--- + httplib.h | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++-- + test/test.cc | 69 +++++++++++++++++++++++++++++++++++++-- + 2 files changed, 156 insertions(+), 5 deletions(-) + +diff --git a/httplib.h b/httplib.h +index 34955002df..e6c55344af 100644 +--- a/httplib.h ++++ b/httplib.h +@@ -450,6 +450,10 @@ struct hash { + } + }; + ++template ++using unordered_set = std::unordered_set; ++ + } // namespace case_ignore + + // This is based on +@@ -710,6 +714,7 @@ struct Request { + std::string matched_route; + Params params; + Headers headers; ++ Headers trailers; + std::string body; + + std::string remote_addr; +@@ -744,6 +749,10 @@ struct Request { + size_t get_header_value_count(const std::string &key) const; + void set_header(const std::string &key, const std::string &val); + ++ bool has_trailer(const std::string &key) const; ++ std::string get_trailer_value(const std::string &key, size_t id = 0) const; ++ size_t get_trailer_value_count(const std::string &key) const; ++ + bool has_param(const std::string &key) const; + std::string get_param_value(const std::string &key, size_t id = 0) const; + size_t get_param_value_count(const std::string &key) const; +@@ -765,6 +774,7 @@ struct Response { + int status = -1; + std::string reason; + Headers headers; ++ Headers trailers; + std::string body; + std::string location; // Redirect location + +@@ -776,6 +786,10 @@ struct Response { + size_t get_header_value_count(const std::string &key) const; + void set_header(const std::string &key, const std::string &val); + ++ bool has_trailer(const std::string &key) const; ++ std::string get_trailer_value(const std::string &key, size_t id = 0) const; ++ size_t get_trailer_value_count(const std::string &key) const; ++ + void set_redirect(const std::string &url, int status = StatusCode::Found_302); + void set_content(const char *s, size_t n, const std::string &content_type); + void set_content(const std::string &s, const std::string &content_type); +@@ -4727,6 +4741,42 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, + // chunked transfer coding data without the final CRLF. + if (!line_reader.getline()) { return ReadContentResult::Success; } + ++ // RFC 7230 Section 4.1.2 - Headers prohibited in trailers ++ thread_local case_ignore::unordered_set prohibited_trailers = { ++ // Message framing ++ "transfer-encoding", "content-length", ++ ++ // Routing ++ "host", ++ ++ // Authentication ++ "authorization", "www-authenticate", "proxy-authenticate", ++ "proxy-authorization", "cookie", "set-cookie", ++ ++ // Request modifiers ++ "cache-control", "expect", "max-forwards", "pragma", "range", "te", ++ ++ // Response control ++ "age", "expires", "date", "location", "retry-after", "vary", "warning", ++ ++ // Payload processing ++ "content-encoding", "content-type", "content-range", "trailer"}; ++ ++ // Parse declared trailer headers once for performance ++ case_ignore::unordered_set declared_trailers; ++ if (has_header(x.headers, "Trailer")) { ++ auto trailer_header = get_header_value(x.headers, "Trailer", "", 0); ++ auto len = std::strlen(trailer_header); ++ ++ split(trailer_header, trailer_header + len, ',', ++ [&](const char *b, const char *e) { ++ std::string key(b, e); ++ if (prohibited_trailers.find(key) == prohibited_trailers.end()) { ++ declared_trailers.insert(key); ++ } ++ }); ++ } ++ + size_t trailer_header_count = 0; + while (strcmp(line_reader.ptr(), "\r\n") != 0) { + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { +@@ -4744,11 +4794,12 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, + + parse_header(line_reader.ptr(), end, + [&](const std::string &key, const std::string &val) { +- x.headers.emplace(key, val); ++ if (declared_trailers.find(key) != declared_trailers.end()) { ++ x.trailers.emplace(key, val); ++ trailer_header_count++; ++ } + }); + +- trailer_header_count++; +- + if (!line_reader.getline()) { return ReadContentResult::Error; } + } + +@@ -6468,6 +6519,24 @@ inline void Request::set_header(const std::string &key, + } + } + ++inline bool Request::has_trailer(const std::string &key) const { ++ return trailers.find(key) != trailers.end(); ++} ++ ++inline std::string Request::get_trailer_value(const std::string &key, ++ size_t id) const { ++ auto rng = trailers.equal_range(key); ++ auto it = rng.first; ++ std::advance(it, static_cast(id)); ++ if (it != rng.second) { return it->second; } ++ return std::string(); ++} ++ ++inline size_t Request::get_trailer_value_count(const std::string &key) const { ++ auto r = trailers.equal_range(key); ++ return static_cast(std::distance(r.first, r.second)); ++} ++ + inline bool Request::has_param(const std::string &key) const { + return params.find(key) != params.end(); + } +@@ -6571,6 +6640,23 @@ inline void Response::set_header(const std::string &key, + headers.emplace(key, val); + } + } ++inline bool Response::has_trailer(const std::string &key) const { ++ return trailers.find(key) != trailers.end(); ++} ++ ++inline std::string Response::get_trailer_value(const std::string &key, ++ size_t id) const { ++ auto rng = trailers.equal_range(key); ++ auto it = rng.first; ++ std::advance(it, static_cast(id)); ++ if (it != rng.second) { return it->second; } ++ return std::string(); ++} ++ ++inline size_t Response::get_trailer_value_count(const std::string &key) const { ++ auto r = trailers.equal_range(key); ++ return static_cast(std::distance(r.first, r.second)); ++} + + inline void Response::set_redirect(const std::string &url, int stat) { + if (detail::fields::is_field_value(url)) { +diff --git a/test/test.cc b/test/test.cc +index 023e92602f..7e397de322 100644 +--- a/test/test.cc ++++ b/test/test.cc +@@ -4886,8 +4886,22 @@ TEST_F(ServerTest, GetStreamedChunkedWithTrailer) { + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ(std::string("123456789"), res->body); +- EXPECT_EQ(std::string("DummyVal1"), res->get_header_value("Dummy1")); +- EXPECT_EQ(std::string("DummyVal2"), res->get_header_value("Dummy2")); ++ ++ EXPECT_TRUE(res->has_header("Trailer")); ++ EXPECT_EQ(1U, res->get_header_value_count("Trailer")); ++ EXPECT_EQ(std::string("Dummy1, Dummy2"), res->get_header_value("Trailer")); ++ ++ // Trailers are now stored separately from headers (security fix) ++ EXPECT_EQ(2U, res->trailers.size()); ++ EXPECT_TRUE(res->has_trailer("Dummy1")); ++ EXPECT_TRUE(res->has_trailer("Dummy2")); ++ EXPECT_FALSE(res->has_trailer("Dummy3")); ++ EXPECT_EQ(std::string("DummyVal1"), res->get_trailer_value("Dummy1")); ++ EXPECT_EQ(std::string("DummyVal2"), res->get_trailer_value("Dummy2")); ++ ++ // Verify trailers are NOT in headers (security verification) ++ EXPECT_EQ(std::string(""), res->get_header_value("Dummy1")); ++ EXPECT_EQ(std::string(""), res->get_header_value("Dummy2")); + } + + TEST_F(ServerTest, LargeChunkedPost) { +@@ -10567,3 +10581,54 @@ TEST(ClientInThreadTest, Issue2068) { + t.join(); + } + } ++ ++TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { ++ Server svr; ++ ++ svr.Get("/", [](const Request &req, Response &res) { ++ EXPECT_EQ(2U, req.trailers.size()); ++ ++ EXPECT_FALSE(req.has_trailer("[invalid key...]")); ++ ++ // Denied ++ EXPECT_FALSE(req.has_trailer("Content-Length")); ++ EXPECT_FALSE(req.has_trailer("X-Forwarded-For")); ++ ++ // Accepted ++ EXPECT_TRUE(req.has_trailer("X-Hello")); ++ EXPECT_EQ(req.get_trailer_value("X-Hello"), "hello"); ++ ++ EXPECT_TRUE(req.has_trailer("X-World")); ++ EXPECT_EQ(req.get_trailer_value("X-World"), "world"); ++ ++ res.set_content("ok", "text/plain"); ++ }); ++ ++ thread t = thread([&]() { svr.listen(HOST, PORT); }); ++ auto se = detail::scope_exit([&] { ++ svr.stop(); ++ t.join(); ++ ASSERT_FALSE(svr.is_running()); ++ }); ++ ++ svr.wait_until_ready(); ++ ++ const std::string req = "GET / HTTP/1.1\r\n" ++ "Transfer-Encoding: chunked\r\n" ++ "Trailer: X-Hello, X-World, X-AAA, X-BBB\r\n" ++ "\r\n" ++ "0\r\n" ++ "Content-Length: 10\r\n" ++ "Host: internal.local\r\n" ++ "Content-Type: malicious/content\r\n" ++ "Cookie: any\r\n" ++ "Set-Cookie: any\r\n" ++ "X-Forwarded-For: attacker.com\r\n" ++ "X-Real-Ip: 1.1.1.1\r\n" ++ "X-Hello: hello\r\n" ++ "X-World: world\r\n" ++ "\r\n"; ++ ++ std::string res; ++ ASSERT_TRUE(send_request(1, req, &res)); ++} diff --git a/cpp-httplib.spec b/cpp-httplib.spec index bbbadb6be3eca354527c4e26b116ba9ef4903e1c..c4822b3bb4bbe9bb69a47339779fa8794504273c 100644 --- a/cpp-httplib.spec +++ b/cpp-httplib.spec @@ -1,10 +1,12 @@ Name: cpp-httplib Version: 0.22.0 -Release: 1 +Release: 2 Summary: A C++ header-only HTTP/HTTPS server and client library License: MIT URL: https://github.com/yhirose/cpp-httplib Source0: https://github.com/yhirose/cpp-httplib/archive/v%{version}/%{name}-%{version}.tar.gz +Patch0: CVE-2025-53629-pre.patch +Patch1: CVE-2025-53629.patch BuildRequires: gcc-c++ BuildRequires: cmake BuildRequires: pkgconfig(libcurl) @@ -66,6 +68,9 @@ cd - %{_libdir}/cmake/httplib %changelog +* Tue Jul 15 2025 yaoxin <1024769339@qq.com> - 0.22.0-2 +- Fix CVE-2025-53629 + * Tue Jul 01 2025 yaoxin <1024769339@qq.com> - 0.22.0-1 - Update to 0.22.0 for fix CVE-2025-52887