This is an automated email from the ASF dual-hosted git repository.

git-hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 3e564ca4d fix(replication): reject unsafe fullsync file names (#3483)
3e564ca4d is described below

commit 3e564ca4d0374130ad00a9c9ec03989c2ef44cf0
Author: hulk <[email protected]>
AuthorDate: Mon May 11 18:44:36 2026 +0800

    fix(replication): reject unsafe fullsync file names (#3483)
    
    Replication fullsync uses the peer's file names to fetch checkpoint
    files
    and materialize them in the local sync checkpoint directory. Previously,
    those names were joined directly with local directories,
    so traversal components could escape the intended directory during
    existence checks,
    temporary file creation, rename, cleanup, or master-side file open.
    
    Assisted-by: Codex/GPT 5.5 xhigh
---
 src/cluster/replication.cc    |  4 ++++
 src/storage/storage.cc        | 47 +++++++++++++++++++++++++++++++++++++++++++
 src/storage/storage.h         |  2 ++
 tests/cppunit/storage_test.cc | 41 +++++++++++++++++++++++++++++++++++++
 4 files changed, 94 insertions(+)

diff --git a/src/cluster/replication.cc b/src/cluster/replication.cc
index f91a6a36e..ca7c414bd 100644
--- a/src/cluster/replication.cc
+++ b/src/cluster/replication.cc
@@ -847,6 +847,10 @@ ReplicationThread::CBState 
ReplicationThread::fullSyncReadCB(bufferevent *bev) {
         }
         std::vector<std::string> need_files = 
util::Split(std::string(line.get()), ",");
         for (const auto &f : need_files) {
+          if (auto s = 
engine::Storage::ReplDataManager::ValidateReplFileName(f); !s.IsOK()) {
+            WARN("[replication] Ignored the invalid fullsync file name '{}': 
{}", f, s.Msg());
+            continue;
+          }
           meta.files.emplace_back(f, 0);
         }
 
diff --git a/src/storage/storage.cc b/src/storage/storage.cc
index 288524eb0..74a9bca3a 100644
--- a/src/storage/storage.cc
+++ b/src/storage/storage.cc
@@ -34,6 +34,7 @@
 #include <iostream>
 #include <memory>
 #include <random>
+#include <string_view>
 
 #include "compact_filter.h"
 #include "db_util.h"
@@ -1226,7 +1227,40 @@ Status 
Storage::ReplDataManager::CleanInvalidFiles(Storage *storage, const std::
   return ret;
 }
 
+Status Storage::ReplDataManager::ValidateReplFileName(const std::string 
&repl_file) {
+  if (repl_file.empty()) {
+    return {Status::NotOK, "empty replication file name"};
+  }
+  if (repl_file.front() == '/') {
+    return {Status::NotOK, fmt::format("absolute replication file name '{}' is 
not allowed", repl_file)};
+  }
+  if (repl_file.back() == '/') {
+    return {Status::NotOK, fmt::format("unsafe replication file name '{}' is 
not allowed", repl_file)};
+  }
+
+  for (size_t begin = 0; begin < repl_file.size();) {
+    auto end = repl_file.find('/', begin);
+    auto component = std::string_view(repl_file).substr(begin, end - begin);
+    if (component.empty() || component == "." || component == "..") {
+      return {Status::NotOK, fmt::format("unsafe replication file name '{}' is 
not allowed", repl_file)};
+    }
+    if (component.find('\0') != std::string_view::npos || component.find('\\') 
!= std::string_view::npos ||
+        component.find(':') != std::string_view::npos) {
+      return {Status::NotOK, fmt::format("unsafe replication file name '{}' is 
not allowed", repl_file)};
+    }
+    if (end == std::string::npos) break;
+    begin = end + 1;
+  }
+
+  return Status::OK();
+}
+
 int Storage::ReplDataManager::OpenDataFile(Storage *storage, const std::string 
&repl_file, uint64_t *file_size) {
+  if (auto s = ValidateReplFileName(repl_file); !s.IsOK()) {
+    ERROR("[storage] Invalid replication data file '{}': {}", repl_file, 
s.Msg());
+    return NullFD;
+  }
+
   std::string abs_path = storage->config_->checkpoint_dir + "/" + repl_file;
   auto s = storage->env_->FileExists(abs_path);
   if (!s.ok()) {
@@ -1282,6 +1316,9 @@ Status Storage::ReplDataManager::ParseMetaAndSave(Storage 
*storage, rocksdb::Bac
     }
 
     auto filename = std::string(line.get(), cptr - line.get() - 1);
+    if (auto s = ValidateReplFileName(filename); !s.IsOK()) {
+      return s;
+    }
     while (*(cptr++) != ' ') {
     }
 
@@ -1311,6 +1348,11 @@ Status MkdirRecursively(rocksdb::Env *env, const 
std::string &dir) {
 
 std::unique_ptr<rocksdb::WritableFile> 
Storage::ReplDataManager::NewTmpFile(Storage *storage, const std::string &dir,
                                                                             
const std::string &repl_file) {
+  if (auto s = ValidateReplFileName(repl_file); !s.IsOK()) {
+    ERROR("[storage] Invalid replication data file '{}': {}", repl_file, 
s.Msg());
+    return nullptr;
+  }
+
   std::string tmp_file = dir + "/" + repl_file + ".tmp";
   auto s = storage->env_->FileExists(tmp_file);
   if (s.ok()) {
@@ -1335,6 +1377,10 @@ std::unique_ptr<rocksdb::WritableFile> 
Storage::ReplDataManager::NewTmpFile(Stor
 }
 
 Status Storage::ReplDataManager::SwapTmpFile(Storage *storage, const 
std::string &dir, const std::string &repl_file) {
+  if (auto s = ValidateReplFileName(repl_file); !s.IsOK()) {
+    return s;
+  }
+
   std::string tmp_file = dir + "/" + repl_file + ".tmp";
   std::string orig_file = dir + "/" + repl_file;
 
@@ -1349,6 +1395,7 @@ Status Storage::ReplDataManager::SwapTmpFile(Storage 
*storage, const std::string
 bool Storage::ReplDataManager::FileExists(Storage *storage, const std::string 
&dir, const std::string &repl_file,
                                           uint32_t crc) {
   if (storage->IsClosing()) return false;
+  if (!ValidateReplFileName(repl_file).IsOK()) return false;
 
   auto file_path = dir + "/" + repl_file;
   auto s = storage->env_->FileExists(file_path);
diff --git a/src/storage/storage.h b/src/storage/storage.h
index 69afeff28..e199052b6 100644
--- a/src/storage/storage.h
+++ b/src/storage/storage.h
@@ -320,6 +320,8 @@ class Storage {
   // Full replication data files manager
   class ReplDataManager {
    public:
+    static Status ValidateReplFileName(const std::string &repl_file);
+
     // Master side
     static Status GetFullReplDataInfo(Storage *storage, std::string *files);
     static int OpenDataFile(Storage *storage, const std::string &rel_file, 
uint64_t *file_size);
diff --git a/tests/cppunit/storage_test.cc b/tests/cppunit/storage_test.cc
index 3b403e376..e6404e5d2 100644
--- a/tests/cppunit/storage_test.cc
+++ b/tests/cppunit/storage_test.cc
@@ -26,6 +26,8 @@
 
 #include <filesystem>
 #include <fstream>
+#include <string>
+#include <vector>
 
 TEST(Storage, CreateBackup) {
   std::error_code ec;
@@ -129,3 +131,42 @@ TEST(Storage, RocksDBDictionaryCompressionOptions) {
 
   unlink(path);
 }
+
+TEST(Storage, ReplDataManagerRejectsUnsafeFilenames) {
+  Config config;
+  config.db_dir = "test_repl_file_validation_dir/db";
+  config.checkpoint_dir = "test_repl_file_validation_dir/checkpoint";
+  config.slot_id_encoded = false;
+
+  std::error_code ec;
+  std::filesystem::remove_all("test_repl_file_validation_dir", ec);
+  ASSERT_FALSE(ec);
+
+  engine::Storage storage(&config);
+  auto base_dir = std::string("test_repl_file_validation_dir/sync_checkpoint");
+
+  auto valid_file = engine::Storage::ReplDataManager::NewTmpFile(&storage, 
base_dir, "meta/1");
+  ASSERT_TRUE(valid_file);
+  ASSERT_TRUE(valid_file->Close().ok());
+  ASSERT_TRUE(engine::Storage::ReplDataManager::SwapTmpFile(&storage, 
base_dir, "meta/1").IsOK());
+  
EXPECT_TRUE(std::filesystem::exists("test_repl_file_validation_dir/sync_checkpoint/meta/1"));
+
+  const std::vector<std::string> unsafe_files = {
+      "../escape", "a/../../escape", "/tmp/escape", "a//b",      "a/",
+      ".",         "a/..",           "a\\b",        "C:/escape", 
std::string("bad\0name", 8),
+  };
+  for (const auto &file : unsafe_files) {
+    
EXPECT_FALSE(engine::Storage::ReplDataManager::ValidateReplFileName(file).IsOK())
 << file;
+    EXPECT_FALSE(engine::Storage::ReplDataManager::NewTmpFile(&storage, 
base_dir, file)) << file;
+    EXPECT_FALSE(engine::Storage::ReplDataManager::SwapTmpFile(&storage, 
base_dir, file).IsOK()) << file;
+    EXPECT_FALSE(engine::Storage::ReplDataManager::FileExists(&storage, 
base_dir, file, 0)) << file;
+    uint64_t file_size = 0;
+    EXPECT_LT(engine::Storage::ReplDataManager::OpenDataFile(&storage, file, 
&file_size), 0) << file;
+  }
+
+  
EXPECT_FALSE(std::filesystem::exists("test_repl_file_validation_dir/escape.tmp"));
+  
EXPECT_FALSE(std::filesystem::exists("test_repl_file_validation_dir/escape"));
+
+  std::filesystem::remove_all("test_repl_file_validation_dir", ec);
+  ASSERT_FALSE(ec);
+}

Reply via email to