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.
This commit is contained in:
yhirose committed 2026-08-13 23:57:28 -04:00
1 parent 89e0c5c238
commit 88240172ea
2 files changed
+66

No files matched your search

+17
View File
@@ -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
View File
@@ -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;