From 88240172ea6c0bde3c74d6bc02958f825a131975 Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 13 Aug 2026 23:57:28 -0400 Subject: [PATCH] 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. --- httplib.h | 17 +++++++++++++++++ test/test.cc | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/httplib.h b/httplib.h index 105a815..25fa42e 100644 --- a/httplib.h +++ b/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_); } diff --git a/test/test.cc b/test/test.cc index fb1cb0f..3bf77e1 100644 --- a/test/test.cc +++ b/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;