From 1e97c28e3609888c02ada9a922e8d51818040b97 Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 12 Mar 2026 23:41:58 -0400 Subject: [PATCH] Implement request smuggling protection for duplicate Content-Length headers and add corresponding tests --- httplib.h | 10 +++++++++ test/test.cc | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/httplib.h b/httplib.h index cb87afdb..df3686d1 100644 --- a/httplib.h +++ b/httplib.h @@ -6813,6 +6813,16 @@ inline bool read_headers(Stream &strm, Headers &headers) { header_count++; } + // RFC 9110 Section 8.6: Reject requests with multiple Content-Length + // headers that have different values to prevent request smuggling. + auto cl_range = headers.equal_range("Content-Length"); + if (cl_range.first != cl_range.second) { + const auto &first_val = cl_range.first->second; + for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) { + if (it->second != first_val) { return false; } + } + } + return true; } diff --git a/test/test.cc b/test/test.cc index 3f9dfcad..d9c371a5 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13051,6 +13051,66 @@ TEST(ClientInThreadTest, Issue2068) { } } +TEST(RequestSmugglingTest, DuplicateContentLengthDifferentValues) { + auto handled = false; + + Server svr; + svr.Post("/test", [&](const Request &, Response &) { handled = true; }); + + thread t = thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + ASSERT_FALSE(handled); + }); + + svr.wait_until_ready(); + + // Two Content-Length headers with different values — must be rejected + auto req = "POST /test HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Content-Length: 10\r\n" + "\r\n" + "hello"; + + std::string response; + ASSERT_TRUE(send_request(1, req, &response)); + ASSERT_EQ("HTTP/1.1 400 Bad Request", + response.substr(0, response.find("\r\n"))); +} + +TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) { + auto handled = false; + + Server svr; + svr.Post("/test", [&](const Request &, Response &res) { + handled = true; + 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()); + ASSERT_TRUE(handled); + }); + + svr.wait_until_ready(); + + // Two Content-Length headers with same value — should be accepted (RFC 9110) + auto req = "POST /test HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; + + std::string response; + ASSERT_TRUE(send_request(1, req, &response)); + ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n"))); +} + TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) { Server svr;