浏览代码

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 <yuji.hirose.bug@gmail.com>
KBS 3 周之前
父节点
当前提交
8d25b6a3ac
共有 2 个文件被更改,包括 25 次插入 和 3 次删除
  1. 10 3
      httplib.h
  2. 15 0
      test/test.cc

+ 10 - 3
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; }

+ 15 - 0
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) {