From 877a52f6b34b1a9d426fc5344f7dbb119548fc2b Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Wed, 15 Jul 2026 10:57:57 -0500 Subject: [PATCH] Limit the number of header lines per multipart form-data part The multipart/form-data parser bounded the length of each individual header line via CPPHTTPLIB_HEADER_MAX_LENGTH but did not cap how many header lines a single part could contain. Within the overall payload limit, a client could pack a large number of small header lines into one part, each of which is parsed twice, inflating CPU usage. Add a per-part header-line counter that resets when a new entry begins and abort parsing once it reaches CPPHTTPLIB_HEADER_MAX_COUNT, mirroring the limit already enforced by read_headers() for request headers. --- httplib.h | 9 +++++++++ test/test.cc | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/httplib.h b/httplib.h index 7f9813e7f9..c0280434c7 100644 --- a/httplib.h +++ b/httplib.h @@ -8095,6 +8095,13 @@ class FormDataParser { break; } + // Check header count limit + if (header_count_ >= CPPHTTPLIB_HEADER_MAX_COUNT) { + is_valid_ = false; + return false; + } + header_count_++; + const auto header = buf_head(pos); if (!parse_header(header.data(), header.data() + header.size(), @@ -8210,6 +8217,7 @@ class FormDataParser { file_.filename.clear(); file_.content_type.clear(); file_.headers.clear(); + header_count_ = 0; } bool start_with_case_ignore(const std::string &a, const char *b, @@ -8263,6 +8271,7 @@ class FormDataParser { size_t state_ = 0; bool is_valid_ = false; FormData file_; + size_t header_count_ = 0; // Buffer bool start_with(const std::string &a, size_t spos, size_t epos, diff --git a/test/test.cc b/test/test.cc index b1d4847622..f456d9a435 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13612,6 +13612,43 @@ TEST(MultipartFormDataTest, MakeFileProvider) { EXPECT_EQ(StatusCode::OK_200, res->status); } +TEST(MultipartFormDataTest, ExcessivePartHeaders) { + // Verify that a multipart part with more than CPPHTTPLIB_HEADER_MAX_COUNT + // (100) headers is rejected with a 400 Bad Request response. + Server svr; + svr.Post("/post", [&](const Request & /*req*/, Response &res) { + 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(); + + // Build a multipart part with 101 custom headers (exceeding CPPHTTPLIB_HEADER_MAX_COUNT=100) + std::stringstream body; + body << "--simpleboundary\r\n" + << "Content-Disposition: form-data; name=\"field1\"\r\n"; + for (int i = 0; i < 101; i++) { + body << "X-Custom-Header-" << i << ": value\r\n"; + } + body << "\r\n" + << "value1\r\n" + << "--simpleboundary--\r\n"; + + std::string content_type = "multipart/form-data; boundary=simpleboundary"; + + Client cli(HOST, PORT); + auto res = cli.Post("/post", body.str(), content_type.c_str()); + + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::BadRequest_400, res->status); +} + TEST(MakeFileBodyTest, Basic) { const std::string file_content(4096, 'Z'); const std::string tmp_path = "./httplib_test_make_file_body.bin";