Explorar o código

Add filename sanitization function and tests to prevent path traversal vulnerabilities

yhirose hai 2 semanas
pai
achega
83e98a28dd
Modificáronse 3 ficheiros con 59 adicións e 4 borrados
  1. 14 4
      README.md
  2. 26 0
      httplib.h
  3. 19 0
      test/test.cc

+ 14 - 4
README.md

@@ -541,16 +541,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
     }
 
     // IMPORTANT: file.filename is an untrusted value from the client.
-    // Always extract only the basename to prevent path traversal attacks.
-    auto safe_name = std::filesystem::path(file.filename).filename();
-    if (safe_name.empty() || safe_name == "." || safe_name == "..") {
+    // Always sanitize to prevent path traversal attacks.
+    auto safe_name = httplib::sanitize_filename(file.filename);
+    if (safe_name.empty()) {
       res.status = StatusCode::BadRequest_400;
       res.set_content("Invalid filename", "text/plain");
       return;
     }
 
     // Save to disk
-    std::ofstream ofs(upload_dir / safe_name, std::ios::binary);
+    std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary);
     ofs << file.content;
   }
 
@@ -586,6 +586,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
 });
 ```
 
+#### Filename Sanitization
+
+`file.filename` in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths:
+
+```cpp
+auto safe = httplib::sanitize_filename(file.filename);
+```
+
+This function strips path separators (`/`, `\`), null bytes, leading/trailing whitespace, and rejects `.` and `..`. Returns an empty string if the filename is unsafe.
+
 ### Receive content with a content receiver
 
 ```cpp

+ 26 - 0
httplib.h

@@ -2915,6 +2915,8 @@ std::string encode_query_component(const std::string &component,
 std::string decode_query_component(const std::string &component,
                                    bool plus_as_space = true);
 
+std::string sanitize_filename(const std::string &filename);
+
 std::string append_query_params(const std::string &path, const Params &params);
 
 std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
@@ -9395,6 +9397,30 @@ inline std::string decode_query_component(const std::string &component,
   return result;
 }
 
+inline std::string sanitize_filename(const std::string &filename) {
+  // Extract basename: find the last path separator (/ or \)
+  auto pos = filename.find_last_of("/\\");
+  auto result =
+      (pos != std::string::npos) ? filename.substr(pos + 1) : filename;
+
+  // Strip null bytes
+  result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
+
+  // Trim whitespace
+  {
+    auto start = result.find_first_not_of(" \t");
+    auto end = result.find_last_not_of(" \t");
+    result = (start == std::string::npos)
+                 ? ""
+                 : result.substr(start, end - start + 1);
+  }
+
+  // Reject . and ..
+  if (result == "." || result == "..") { return ""; }
+
+  return result;
+}
+
 inline std::string append_query_params(const std::string &path,
                                        const Params &params) {
   std::string path_with_query = path;

+ 19 - 0
test/test.cc

@@ -413,6 +413,25 @@ TEST(DecodePathTest, PercentCharacterNUL) {
   EXPECT_EQ(decode_path_component("x%00x"), expected);
 }
 
+TEST(SanitizeFilenameTest, VariousPatterns) {
+  // Path traversal
+  EXPECT_EQ("passwd", httplib::sanitize_filename("../../../etc/passwd"));
+  EXPECT_EQ("passwd", httplib::sanitize_filename("..\\..\\etc\\passwd"));
+  EXPECT_EQ("file.txt", httplib::sanitize_filename("path/to\\..\\file.txt"));
+  // Normal and edge cases
+  EXPECT_EQ("photo.jpg", httplib::sanitize_filename("photo.jpg"));
+  EXPECT_EQ("filename.txt",
+            httplib::sanitize_filename("/path/to/filename.txt"));
+  EXPECT_EQ(".gitignore", httplib::sanitize_filename(".gitignore"));
+  EXPECT_EQ("", httplib::sanitize_filename(".."));
+  EXPECT_EQ("", httplib::sanitize_filename(""));
+  // Null bytes stripped
+  EXPECT_EQ("safe.txt",
+            httplib::sanitize_filename(std::string("safe\0.txt", 9)));
+  // Whitespace-only rejected
+  EXPECT_EQ("", httplib::sanitize_filename("   "));
+}
+
 TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
   string unescapedCharacters = "-_.!~*'()";