This is an automated email from the ASF dual-hosted git repository.
adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git
The following commit(s) were added to refs/heads/master by this push:
new 6e2cafb6b RATIS-2304. SnapshotManager should validate snapshot file
path (#1268)
6e2cafb6b is described below
commit 6e2cafb6b7c18e7322079e0fa0ca2548a3bcd55f
Author: Tsz-Wo Nicholas Sze <[email protected]>
AuthorDate: Thu May 22 00:59:14 2025 -0700
RATIS-2304. SnapshotManager should validate snapshot file path (#1268)
---
.../main/java/org/apache/ratis/util/FileUtils.java | 33 ++++++++++++++++++++++
.../java/org/apache/ratis/util/TestFileUtils.java | 26 +++++++++++++++++
.../ratis/server/storage/SnapshotManager.java | 8 +++---
3 files changed, 63 insertions(+), 4 deletions(-)
diff --git a/ratis-common/src/main/java/org/apache/ratis/util/FileUtils.java
b/ratis-common/src/main/java/org/apache/ratis/util/FileUtils.java
index 4b9d9e3b2..315010c62 100644
--- a/ratis-common/src/main/java/org/apache/ratis/util/FileUtils.java
+++ b/ratis-common/src/main/java/org/apache/ratis/util/FileUtils.java
@@ -60,6 +60,39 @@ public interface FileUtils {
}
}
+ /** @return true iff the given dir is an ancestor of the given sub path. */
+ static boolean isAncestor(File dir, File sub) throws IOException {
+ Objects.requireNonNull(dir, "dir == null");
+ Objects.requireNonNull(sub, "sub == null");
+
+ String dirPath = dir.getCanonicalPath();
+ final String subPath = sub.getCanonicalPath();
+ if (dirPath.equals(subPath)) {
+ return true;
+ } else if (!dirPath.endsWith(File.separator)) {
+ dirPath += File.separator;
+ }
+ LOG.debug("dirPath: {}", dirPath);
+ LOG.debug("subPath: {}", subPath);
+ return subPath.startsWith(dirPath);
+ }
+
+ /**
+ * Resolve the full path from the given dir and sub,
+ * where dir is supposed to be an ancestor of the resolved path.
+ *
+ * @return the full path
+ * @throws IOException if the dir is not an ancestor of the resolved path.
+ */
+ static File resolveFullPath(File dir, String sub) throws IOException {
+ final File full = new File(dir, sub);
+ if (!isAncestor(dir, full)) {
+ throw new IOException("The dir is not an ancestor of the full path:
dir=" + dir
+ + ", sub=" + sub + ", full=" + full);
+ }
+ return full;
+ }
+
static void truncateFile(File f, long target) throws IOException {
final long original = f.length();
LogUtils.runAndLog(LOG,
diff --git
a/ratis-common/src/test/java/org/apache/ratis/util/TestFileUtils.java
b/ratis-common/src/test/java/org/apache/ratis/util/TestFileUtils.java
index 3171756b8..8f64ff294 100644
--- a/ratis-common/src/test/java/org/apache/ratis/util/TestFileUtils.java
+++ b/ratis-common/src/test/java/org/apache/ratis/util/TestFileUtils.java
@@ -26,6 +26,32 @@ import java.io.IOException;
/** Test methods of {@link FileUtils}. */
public class TestFileUtils extends BaseTest {
+ @Test
+ public void testIsAncestor() throws IOException {
+ runTestIsAncestor(true, "/a", "/a/b");
+ runTestIsAncestor(true, "/a", "/a/");
+ runTestIsAncestor(true, "/a", "/a");
+ runTestIsAncestor(true, "a", "a/b");
+ runTestIsAncestor(true, "a", "a/");
+ runTestIsAncestor(true, "a", "a");
+
+ runTestIsAncestor(false, "/a", "/c");
+ runTestIsAncestor(false, "/a", "/abc");
+ runTestIsAncestor(false, "/a", "/a/../c");
+ runTestIsAncestor(false, "a", "a/../c");
+ runTestIsAncestor(false, "a", "/c");
+ }
+
+ static void runTestIsAncestor(boolean expected, String ancestor, String
path) throws IOException {
+ final boolean computed = isAncestor(ancestor, path);
+ System.out.printf("isAncestor(%2s, %-9s)? %s, expected? %s%n",
+ ancestor, path, computed, expected);
+ Assertions.assertSame(expected, computed);
+ }
+
+ static boolean isAncestor(String ancestor, String path) throws IOException {
+ return FileUtils.isAncestor(new File(ancestor), new File(path));
+ }
@Test
public void testRenameToCorrupt() throws IOException {
diff --git
a/ratis-server/src/main/java/org/apache/ratis/server/storage/SnapshotManager.java
b/ratis-server/src/main/java/org/apache/ratis/server/storage/SnapshotManager.java
index 91c1ba5b9..2d10c53a4 100644
---
a/ratis-server/src/main/java/org/apache/ratis/server/storage/SnapshotManager.java
+++
b/ratis-server/src/main/java/org/apache/ratis/server/storage/SnapshotManager.java
@@ -31,6 +31,7 @@ import org.apache.ratis.util.MD5FileUtil;
import org.apache.ratis.util.MemoizedSupplier;
import org.apache.ratis.util.Preconditions;
import org.apache.ratis.util.StringUtils;
+import org.apache.ratis.util.function.CheckedFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +44,6 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.Optional;
-import java.util.function.Function;
import java.util.function.Supplier;
/**
@@ -61,7 +61,7 @@ public class SnapshotManager {
private final Supplier<File> snapshotDir;
private final Supplier<File> snapshotTmpDir;
- private final Function<FileChunkProto, String> getRelativePath;
+ private final CheckedFunction<FileChunkProto, String, IOException>
getRelativePath;
private MessageDigest digester;
SnapshotManager(RaftPeerId selfId, Supplier<RaftStorageDirectory> dir,
StateMachineStorage smStorage) {
@@ -73,7 +73,7 @@ public class SnapshotManager {
final Supplier<Path> smDir = MemoizedSupplier.valueOf(() ->
dir.get().getStateMachineDir().toPath());
this.getRelativePath = c -> smDir.get().relativize(
- new File(dir.get().getRoot(), c.getFilename()).toPath()).toString();
+ FileUtils.resolveFullPath(dir.get().getRoot(),
c.getFilename()).toPath()).toString();
}
@SuppressWarnings({"squid:S2095"}) // Suppress closeable warning
@@ -121,7 +121,7 @@ public class SnapshotManager {
+ " with endIndex >= lastIncludedIndex " + lastIncludedIndex);
}
- final File tmpSnapshotFile = new File(tmpDir,
getRelativePath.apply(chunk));
+ final File tmpSnapshotFile = FileUtils.resolveFullPath(tmpDir,
getRelativePath.apply(chunk));
FileUtils.createDirectoriesDeleteExistingNonDirectory(tmpSnapshotFile.getParentFile());
try (FileChannel out = open(chunk, tmpSnapshotFile)) {