From 11daa99c86954e77a497fa90c4014d4543565aca Mon Sep 17 00:00:00 2001 From: yhirose Date: Mon, 12 Oct 2026 21:27:27 -0400 Subject: [PATCH] Stop at the last match when getting a value by index get_header_value(), get_header_value_u64(), get_multimap_value() and MultipartFormData::get_field() advanced an equal_range() iterator by the caller's index after casting it to ssize_t. An index that is negative as a signed value stepped back onto an entry of another key, and a huge positive one kept looping after the last match. Advance with bounded_next(), which stops at the end of the range. --- httplib.h | 23 +++++++++++++---------- test/test.cc | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/httplib.h b/httplib.h index d758231..e6b2fce 100644 --- a/httplib.h +++ b/httplib.h @@ -1141,8 +1141,7 @@ private: pointer operator->() const { return data_ + idx_; } iterator_t &operator++() { - // Saturating, so that advancing past the last entry of a key (which - // get_multimap_value() does when asked for an out-of-range id) stays at + // Saturating, so that advancing past the last entry of a key stays at // end() instead of running off the container. if (idx_ >= size_) { return *this; } ++idx_; @@ -3477,13 +3476,20 @@ inline bool is_numeric(const std::string &str) { return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit); } +// std::ranges::next(first, n, last) for C++11 +template inline It bounded_next(It first, size_t n, It last) { + for (; n > 0 && first != last; --n) { + ++first; + } + return first; +} + inline size_t get_header_value_u64(const Headers &headers, const std::string &key, size_t def, size_t id, bool &is_invalid_value) { is_invalid_value = false; auto rng = headers.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); + auto it = bounded_next(rng.first, id, rng.second); if (it != rng.second) { if (is_numeric(it->second)) { // Parse at size_t width so an out-of-range Content-Length is reported @@ -8146,8 +8152,7 @@ inline const char *get_header_value(const Headers &headers, } auto rng = headers.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); + auto it = bounded_next(rng.first, id, rng.second); if (it != rng.second) { return it->second.c_str(); } return def; } @@ -8200,8 +8205,7 @@ template inline typename Map::mapped_type get_multimap_value(const Map &m, const std::string &key, size_t id) { auto rng = m.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); + auto it = bounded_next(rng.first, id, rng.second); if (it != rng.second) { return it->second; } return typename Map::mapped_type(); } @@ -11728,8 +11732,7 @@ inline bool Request::is_multipart_form_data() const { inline std::string MultipartFormData::get_field(const std::string &key, size_t id) const { auto rng = fields.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); + auto it = detail::bounded_next(rng.first, id, rng.second); if (it != rng.second) { return it->second.content; } return std::string(); } diff --git a/test/test.cc b/test/test.cc index 09b3129..ede009a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1923,6 +1923,28 @@ TEST(HeadersOrderTest, IdBeyondTheLastDuplicateYieldsDefault) { EXPECT_EQ("def", req.get_header_value("X-Missing", "def", 3)); } +TEST(HeadersOrderTest, HugeIdYieldsDefault) { + Request req; + req.set_header("X-First", "1"); + req.set_header("X-Test", "2"); + req.params.emplace("first", "1"); + req.params.emplace("test", "2"); + req.form.fields.emplace("first", FormField{"first", "1", {}}); + req.form.fields.emplace("test", FormField{"test", "2", {}}); + req.form.files.emplace("first", FormData{"first", "1", "", "", {}}); + req.form.files.emplace("test", FormData{"test", "2", "", "", {}}); + + // The second one is negative when read as a signed value + const auto max_id = (std::numeric_limits::max)(); + for (auto id : {max_id / 2, max_id}) { + EXPECT_EQ("def", req.get_header_value("X-Test", "def", id)); + EXPECT_EQ(7U, req.get_header_value_u64("X-Test", 7, id)); + EXPECT_EQ("", req.get_param_value("test", id)); + EXPECT_EQ("", req.form.get_field("test", id)); + EXPECT_EQ("", req.form.get_file("test", id).content); + } +} + TEST(HeadersOrderTest, TraversalFollowsInsertionOrder) { Headers headers; headers.emplace("Host", "example.com");