From 28dcf379e82a2cdb544d812696a7fd46067eb7f9 Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 24 Jun 2025 07:56:00 -0400 Subject: [PATCH] Merge commit from fork --- httplib.h | 17 ++++++++++++ test/test.cc | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/httplib.h b/httplib.h index a5518ae..b1694cf 100644 --- a/httplib.h +++ b/httplib.h @@ -90,6 +90,10 @@ #define CPPHTTPLIB_HEADER_MAX_LENGTH 8192 #endif +#ifndef CPPHTTPLIB_HEADER_MAX_COUNT +#define CPPHTTPLIB_HEADER_MAX_COUNT 100 +#endif + #ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT #define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 #endif @@ -4355,6 +4359,8 @@ inline bool read_headers(Stream &strm, Headers &headers) { char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); + size_t header_count = 0; + for (;;) { if (!line_reader.getline()) { return false; } @@ -4375,6 +4381,9 @@ inline bool read_headers(Stream &strm, Headers &headers) { if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + // Check header count limit + if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } + // Exclude line terminator auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; @@ -4384,6 +4393,8 @@ inline bool read_headers(Stream &strm, Headers &headers) { })) { return false; } + + header_count++; } return true; @@ -4486,9 +4497,13 @@ inline bool read_content_chunked(Stream &strm, T &x, // chunked transfer coding data without the final CRLF. if (!line_reader.getline()) { return true; } + size_t trailer_header_count = 0; while (strcmp(line_reader.ptr(), "\r\n") != 0) { if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + // Check trailer header count limit + if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } + // Exclude line terminator constexpr auto line_terminator_len = 2; auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; @@ -4498,6 +4513,8 @@ inline bool read_content_chunked(Stream &strm, T &x, x.headers.emplace(key, val); }); + trailer_header_count++; + if (!line_reader.getline()) { return false; } } diff --git a/test/test.cc b/test/test.cc index 3185906..df52529 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3,7 +3,11 @@ #include #ifndef _WIN32 +#include #include +#include +#include +#include #endif #include @@ -3823,6 +3827,50 @@ TEST_F(ServerTest, TooLongHeader) { EXPECT_EQ(StatusCode::OK_200, res->status); } +TEST_F(ServerTest, HeaderCountAtLimit) { + // Test with headers just under the 100 limit + httplib::Headers headers; + + // Add 95 custom headers (the client will add Host, User-Agent, Accept, etc.) + // This should keep us just under the 100 header limit + for (int i = 0; i < 95; i++) { + std::string name = "X-Test-Header-" + std::to_string(i); + std::string value = "value" + std::to_string(i); + headers.emplace(name, value); + } + + // This should work fine as we're under the limit + auto res = cli_.Get("/hi", headers); + EXPECT_TRUE(res); + if (res) { + EXPECT_EQ(StatusCode::OK_200, res->status); + } +} + +TEST_F(ServerTest, HeaderCountExceedsLimit) { + // Test with many headers to exceed the 100 limit + httplib::Headers headers; + + // Add 150 headers to definitely exceed the 100 limit + for (int i = 0; i < 150; i++) { + std::string name = "X-Test-Header-" + std::to_string(i); + std::string value = "value" + std::to_string(i); + headers.emplace(name, value); + } + + // This should fail due to exceeding header count limit + auto res = cli_.Get("/hi", headers); + + // The request should either fail or return 400 Bad Request + if (res) { + // If we get a response, it should be 400 Bad Request + EXPECT_EQ(StatusCode::BadRequest_400, res->status); + } else { + // Or the request should fail entirely + EXPECT_FALSE(res); + } +} + TEST_F(ServerTest, PercentEncoding) { auto res = cli_.Get("/e%6edwith%"); ASSERT_TRUE(res); @@ -3860,6 +3908,32 @@ TEST_F(ServerTest, PlusSignEncoding) { EXPECT_EQ("a +b", res->body); } +TEST_F(ServerTest, HeaderCountSecurityTest) { + // This test simulates a potential DoS attack using many headers + // to verify our security fix prevents memory exhaustion + + httplib::Headers attack_headers; + + // Attempt to add many headers like an attacker would (200 headers to far exceed limit) + for (int i = 0; i < 200; i++) { + std::string name = "X-Attack-Header-" + std::to_string(i); + std::string value = "attack_payload_" + std::to_string(i); + attack_headers.emplace(name, value); + } + + // Try to POST with excessive headers + auto res = cli_.Post("/", attack_headers, "test_data", "text/plain"); + + // Should either fail or return 400 Bad Request due to security limit + if (res) { + // If we get a response, it should be 400 Bad Request + EXPECT_EQ(StatusCode::BadRequest_400, res->status); + } else { + // Request failed, which is the expected behavior for DoS protection + EXPECT_FALSE(res); + } +} + TEST_F(ServerTest, MultipartFormData) { MultipartFormDataItems items = { {"text1", "text default", "", ""},