From 8d25b6a3ac14f86b41af1720d6d2c32bf3533604 Mon Sep 17 00:00:00 2001 From: KBS Date: Sat, 12 Sep 2026 02:48:04 +0900 Subject: [PATCH] Reject a Range first-byte-pos that overflows ssize_t (#2580) * Reject a Range first-byte-pos that overflows ssize_t parse_range_header initializes first to the -1 sentinel that means "no first-byte-pos" and only overwrites it when detail::from_chars succeeds. On std::errc::result_out_of_range the assignment is skipped and -1 survives, so "bytes=9223372036854775808-100" is parsed as the suffix range "bytes=-100" and range_error serves the last 100 bytes instead of returning 416. Before the parser was rewritten onto detail::from_chars, std::stoll threw std::out_of_range on the same input, the catch arm added in 8f8761e for issue #705 returned false, and the request was answered with 416. The catch arm is still there but from_chars reports through an error code, so nothing reaches it any more. get_header_value_u64 and parse_port already reject an out-of-range value at their from_chars call sites; this was the remaining one that dropped the error. The last-byte-pos side is deliberately unchanged: -1 there is the documented RFC 9110 14.1.2 "remainder of the representation" value, so an oversized last-byte-pos stays accepted. * Simplify the Range first-byte-pos overflow check Parse the first-byte-pos straight into first, since a failed parse now returns before first is read, and fold the overflow test into the existing batch of rejected ranges. Also note on the last-byte-pos side why an overflow there deliberately keeps -1. Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi --------- Co-authored-by: yhirose --- httplib.h | 13 ++++++++++--- test/test.cc | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index 3ee379ee..c13e74ae 100644 --- a/httplib.h +++ b/httplib.h @@ -9049,13 +9049,20 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try { ssize_t first = -1; if (!lhs.empty()) { - ssize_t v; - auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v); - if (res.ec == std::errc{}) { first = v; } + // Reject an overflowing first-byte-pos; treating it as absent (-1) + // would turn the range into a suffix range. + auto res = + detail::from_chars(lhs.data(), lhs.data() + lhs.size(), first); + if (res.ec != std::errc{}) { + all_valid_ranges = false; + return; + } } ssize_t last = -1; if (!rhs.empty()) { + // An overflowing last-byte-pos is past any content length, so keeping + // -1 ("remainder", RFC 9110 14.1.2) is correct here. ssize_t v; auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v); if (res.ec == std::errc{}) { last = v; } diff --git a/test/test.cc b/test/test.cc index f27d5cf8..7f3b013a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -2010,8 +2010,23 @@ TEST(ParseHeaderValueTest, Range) { EXPECT_FALSE(detail::parse_range_header("bytes=0--1", ranges)); EXPECT_FALSE(detail::parse_range_header("bytes=0- 1", ranges)); EXPECT_FALSE(detail::parse_range_header("bytes=0 -1", ranges)); + // Overflows ssize_t; must not be read as the suffix range "bytes=-100". + EXPECT_FALSE( + detail::parse_range_header("bytes=9223372036854775808-100", ranges)); EXPECT_TRUE(ranges.empty()); } + + { + // RFC 9110 14.1.2: a last-byte-pos greater than the content length is the + // remainder of the representation, so it stays accepted. + Ranges ranges; + auto ret = + detail::parse_range_header("bytes=0-99999999999999999999", ranges); + EXPECT_TRUE(ret); + ASSERT_EQ(1u, ranges.size()); + EXPECT_EQ(0, ranges[0].first); + EXPECT_EQ(-1, ranges[0].second); + } } TEST(ParseAcceptEncoding1, AcceptEncoding) {