From 578e4815b7de069addd2b61b30f001b5158f5849 Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Wed, 28 Oct 2026 03:46:21 +0530 Subject: [PATCH] Treat ':' as a path separator in sanitize_filename on Windows (#2651) On Windows a ':' ends a drive prefix or starts an NTFS stream name, so a filename such as "C:.." or "a.txt:stream" survived sanitize_filename and could replace the upload directory or write to an alternate data stream. Cut at ':' on Windows as well. --- README.md | 2 +- httplib.h | 7 ++++++- test/test.cc | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0853b797..3c1304cb 100644 --- a/README.md +++ b/README.md @@ -742,7 +742,7 @@ svr.Post("/multipart", [&](const Request& req, Response& res) { auto safe = httplib::sanitize_filename(file.filename); ``` -This function keeps only the part after the last `/` or `\`, removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`. +This function keeps only the part after the last `/` or `\` (or, on Windows, `:`, which starts a drive prefix such as `C:` or an NTFS stream name), removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`. > [!NOTE] > `name` and `filename` are read as quoted-strings (RFC 9110 ยง5.6.4), where `\"` and `\\` stand for `"` and `\`. Browsers send a backslash as it is, so from a browser, a value with `\\` in it arrives with a single `\`, and one that ends in `\` is not read correctly. diff --git a/httplib.h b/httplib.h index fd3074dd..31decafe 100644 --- a/httplib.h +++ b/httplib.h @@ -11975,8 +11975,13 @@ inline std::string decode_query_component(const std::string &component, } inline std::string sanitize_filename(const std::string &filename) { - // Extract basename: find the last path separator (/ or \) + // Extract basename: find the last path separator (/ or \) and, on Windows, + // ':', which ends a drive prefix ("C:..") or names an NTFS stream ("a:b") +#ifdef _WIN32 + auto pos = filename.find_last_of("/\\:"); +#else auto pos = filename.find_last_of("/\\"); +#endif auto result = (pos != std::string::npos) ? filename.substr(pos + 1) : filename; diff --git a/test/test.cc b/test/test.cc index 7b6e8445..e0418e4b 100644 --- a/test/test.cc +++ b/test/test.cc @@ -508,6 +508,20 @@ TEST(SanitizeFilenameTest, VariousPatterns) { EXPECT_EQ("", httplib::sanitize_filename(" ")); } +// On Windows a ':' ends a drive prefix or names an NTFS stream, so what comes +// before it is dropped; anywhere else it is an ordinary file name character. +TEST(SanitizeFilenameTest, ColonEndsPrefixOnlyOnWindows) { +#ifdef _WIN32 + EXPECT_EQ("", httplib::sanitize_filename("C:..")); + EXPECT_EQ("evil.dll", httplib::sanitize_filename("C:evil.dll")); + EXPECT_EQ("stream", httplib::sanitize_filename("a.txt:stream")); +#else + EXPECT_EQ("C:..", httplib::sanitize_filename("C:..")); + EXPECT_EQ("C:evil.dll", httplib::sanitize_filename("C:evil.dll")); + EXPECT_EQ("a.txt:stream", httplib::sanitize_filename("a.txt:stream")); +#endif +} + // Forward declaration (see the base64_encode note below): split builds move the // definition into httplib.cc, so re-declaring it here lets the test link. namespace httplib {