Procházet zdrojové kódy

Serve no file whose path cannot be resolved

When the real path of a file under a mount point could not be resolved,
the check that it stays inside the mount point was skipped and the file
was served. Do not serve it from there, and refuse a mount point whose
directory cannot be resolved.
yhirose před 1 dnem
rodič
revize
eeaae4bc3c
2 změnil soubory, kde provedl 46 přidání a 15 odebrání
  1. 20 15
      httplib.h
  2. 26 0
      test/test.cc

+ 20 - 15
httplib.h

@@ -13340,16 +13340,19 @@ inline bool Server::set_mount_point(const std::string &mount_point,
   if (stat.is_dir()) {
     std::string mnt = !mount_point.empty() ? mount_point : "/";
     if (!mnt.empty() && mnt[0] == '/') {
+      // Without the resolved directory, a file could not be checked to be
+      // inside it.
       std::string resolved_base;
-      if (detail::canonicalize_path(dir.c_str(), resolved_base)) {
+      if (!detail::canonicalize_path(dir.c_str(), resolved_base)) {
+        return false;
+      }
 #if defined(_WIN32)
-        if (resolved_base.back() != '\\' && resolved_base.back() != '/') {
-          resolved_base += '\\';
-        }
+      if (resolved_base.back() != '\\' && resolved_base.back() != '/') {
+        resolved_base += '\\';
+      }
 #else
-        if (resolved_base.back() != '/') { resolved_base += '/'; }
+      if (resolved_base.back() != '/') { resolved_base += '/'; }
 #endif
-      }
       base_dirs_.push_back(
           {std::move(mnt), dir, std::move(resolved_base), std::move(headers)});
       return true;
@@ -14017,15 +14020,17 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
         if (path.back() == '/') { path += "index.html"; }
 
         // Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
-        // but symlinks/junctions can still escape the base directory.
-        if (!entry.resolved_base_dir.empty()) {
-          std::string resolved_path;
-          if (detail::canonicalize_path(path.c_str(), resolved_path) &&
-              !detail::is_path_within_base(resolved_path,
-                                           entry.resolved_base_dir)) {
-            res.status = StatusCode::Forbidden_403;
-            return true;
-          }
+        // but symlinks/junctions can still escape the base directory. A path
+        // that cannot be resolved, which includes one that does not exist, is
+        // not served from here.
+        std::string resolved_path;
+        if (!detail::canonicalize_path(path.c_str(), resolved_path)) {
+          continue;
+        }
+        if (!detail::is_path_within_base(resolved_path,
+                                         entry.resolved_base_dir)) {
+          res.status = StatusCode::Forbidden_403;
+          return true;
         }
 
         detail::FileStat stat(path);

+ 26 - 0
test/test.cc

@@ -11720,6 +11720,32 @@ TEST(MountTest, Unmount) {
   EXPECT_EQ(StatusCode::NotFound_404, res->status);
 }
 
+TEST(MountTest, MissingFileFallsThroughToHandlers) {
+  Server svr;
+  ASSERT_TRUE(svr.set_mount_point("/", "./www"));
+  svr.Get("/api/hello", [](const Request &, Response &res) {
+    res.set_content("api", "text/plain");
+  });
+
+  auto listen_thread = std::thread([&svr]() { svr.listen(HOST, PORT); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    listen_thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  Client cli(HOST, PORT);
+  auto res = cli.Get("/dir/test.html");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+
+  res = cli.Get("/api/hello");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ("api", res->body);
+}
+
 TEST(MountTest, PathSegmentBoundary) {
   Server svr;