فهرست منبع

Guard regex routes against stack overflow from long paths

RegexMatcher::match() called std::regex_match() directly on the
attacker-controlled request path. For quantified patterns such as "(.*)",
std::regex_match's recursive backtracking implementation (most acute on
libstdc++) recurses roughly once per matched character, so a long enough
path can exhaust the calling thread's stack and crash the process. Verified
against real GNU libstdc++: under the default thread stack size, a path of
a couple thousand characters against a simple quantified route pattern
reliably crashed the process, well within the existing 8192-byte request
URI limit.

Add CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH (default 256) and reject paths
longer than it before ever calling std::regex_match, treating them as a
non-match instead. Confirmed the fix eliminates the crash under the same
libstdc++ build and default stack size that reproduced it.
yhirose 6 روز پیش
والد
کامیت
88240172ea
2فایلهای تغییر یافته به همراه66 افزوده شده و 0 حذف شده
  1. 17 0
      httplib.h
  2. 49 0
      test/test.cc

+ 17 - 0
httplib.h

@@ -138,6 +138,18 @@
 #define CPPHTTPLIB_RANGE_MAX_COUNT 1024
 #endif
 
+// std::regex_match's backtracking implementation (most acutely on libstdc++)
+// recurses roughly once per matched character for quantified patterns such
+// as "(.*)", so a long enough path can exhaust the calling thread's stack; on
+// a default ~8MB thread stack that has been observed to take on the order of
+// a couple thousand characters for a simple pattern. 256 leaves a wide safety
+// margin below that (well under the 8192-byte request URI limit) while still
+// fitting any realistic route segment; raise it if a route legitimately needs
+// longer paths. Regex routes are never applied to paths longer than this.
+#ifndef CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH
+#define CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH 256
+#endif
+
 #ifndef CPPHTTPLIB_TCP_NODELAY
 #define CPPHTTPLIB_TCP_NODELAY false
 #endif
@@ -11633,6 +11645,11 @@ inline bool PathParamsMatcher::match(Request &request) const {
 
 inline bool RegexMatcher::match(Request &request) const {
   request.path_params.clear();
+  // See CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH: an overlong path is treated as
+  // a non-match rather than risking a stack overflow in std::regex_match.
+  if (request.path.length() > CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH) {
+    return false;
+  }
   return std::regex_match(request.path, request.matches, regex_);
 }
 

+ 49 - 0
test/test.cc

@@ -15412,6 +15412,55 @@ TEST(PathParamsTest, SemicolonInTheMiddleIsNotAParam) {
   EXPECT_EQ(request.path_params, expected_params);
 }
 
+TEST(RegexMatcherTest, OverlongPathIsRejectedBeforeRegexMatch) {
+  // std::regex_match's backtracking (most acute on libstdc++) can exhaust the
+  // calling thread's stack on a long enough path for a quantified pattern;
+  // RegexMatcher must refuse to run regex matching on paths beyond
+  // CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH instead of invoking it.
+  detail::RegexMatcher matcher("/files/(.*)");
+
+  Request within_limit;
+  within_limit.path = "/files/" + std::string(10, 'A');
+  EXPECT_TRUE(matcher.match(within_limit));
+
+  Request over_limit;
+  over_limit.path =
+      "/files/" + std::string(CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH, 'A');
+  EXPECT_FALSE(matcher.match(over_limit));
+}
+
+TEST(RegexMatcherTest, ServerSurvivesOverlongPathOnRegexRoute) {
+  Server svr;
+  svr.Get(R"(/files/(.*))", [](const Request & /*req*/, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+
+  auto port = svr.bind_to_any_port(HOST);
+  auto thread = std::thread([&]() { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  // Comfortably past CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH, well within the
+  // request URI limit; this used to be able to crash the process.
+  std::string path =
+      "/files/" + std::string(CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH * 4, 'A');
+  std::string req = "GET " + path +
+                    " HTTP/1.1\r\n"
+                    "Host: " +
+                    std::string(HOST) + ":" + std::to_string(port) +
+                    "\r\n"
+                    "Connection: close\r\n"
+                    "\r\n";
+
+  std::string response;
+  ASSERT_TRUE(send_request(5, req, &response, port));
+  EXPECT_NE(std::string::npos, response.find("404"));
+}
+
 TEST(ParseUrlTest, VariousPatterns) {
   {
     detail::UrlComponents uc;