You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ju...@apache.org on 2026/05/13 02:42:03 UTC

(iotdb) branch ger-shop-2 created (now 6686cd5bd47)

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

Caideyipi pushed a change to branch ger-shop-2
in repository https://gitbox.apache.org/repos/asf/iotdb.git


      at 6686cd5bd47 Update LoadTsFileStatementTest.java

This branch includes the following new commits:

     new 7510f23a327 Load: Harden LOAD TSFILE source path validation (#17624)
     new fb64020e707 Update LoadTsFileStatementTest.java
     new 6686cd5bd47 Update LoadTsFileStatementTest.java

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(iotdb) 02/03: Update LoadTsFileStatementTest.java

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Caideyipi pushed a commit to branch ger-shop-2
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit fb64020e7076c062ccfb9dc349e6badcadb80b34
Author: Caideyipi <87...@users.noreply.github.com>
AuthorDate: Wed May 13 10:41:44 2026 +0800

    Update LoadTsFileStatementTest.java
---
 .../statement/crud/LoadTsFileStatementTest.java    | 27 ----------------------
 1 file changed, 27 deletions(-)

diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
index bfebf51d281..da6c4b2cfda 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
@@ -35,33 +35,6 @@ import java.util.stream.Stream;
 
 public class LoadTsFileStatementTest {
 
-  @Test
-  public void testSubStatementsKeepDatabase() throws Exception {
-    final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
-    final int originalBatchSize = config.getLoadTsFileSubStatementBatchSize();
-    final String[] originalAllowedDirs = config.getLoadTsFileAllowedDirs().clone();
-    final Path tempDir = Files.createTempDirectory("load-tsfile-sub-statements");
-
-    try {
-      config.setLoadTsFileSubStatementBatchSize(1);
-      config.setLoadTsFileAllowedDirs(new String[] {tempDir.toString()});
-      Files.createFile(tempDir.resolve("a.tsfile"));
-      Files.createFile(tempDir.resolve("b.tsfile"));
-
-      final LoadTsFileStatement statement = new LoadTsFileStatement(tempDir.toString());
-      statement.setDatabase("test_db");
-
-      final List<LoadTsFileStatement> subStatements = statement.getSubStatements();
-      Assert.assertEquals(2, subStatements.size());
-      subStatements.forEach(
-          subStatement -> Assert.assertEquals("test_db", subStatement.getDatabase()));
-    } finally {
-      config.setLoadTsFileSubStatementBatchSize(originalBatchSize);
-      config.setLoadTsFileAllowedDirs(originalAllowedDirs);
-      deleteRecursively(tempDir);
-    }
-  }
-
   @Test
   public void testLoadSourcePathMustBeInAllowedDirs() throws Exception {
     final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();


(iotdb) 03/03: Update LoadTsFileStatementTest.java

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Caideyipi pushed a commit to branch ger-shop-2
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 6686cd5bd4716f111be480176f947b757e4dd9e2
Author: Caideyipi <87...@users.noreply.github.com>
AuthorDate: Wed May 13 10:41:48 2026 +0800

    Update LoadTsFileStatementTest.java
---
 .../db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java      | 1 -
 1 file changed, 1 deletion(-)

diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
index da6c4b2cfda..acf608abf80 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
@@ -30,7 +30,6 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Comparator;
-import java.util.List;
 import java.util.stream.Stream;
 
 public class LoadTsFileStatementTest {


(iotdb) 01/03: Load: Harden LOAD TSFILE source path validation (#17624)

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Caideyipi pushed a commit to branch ger-shop-2
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 7510f23a3277d72efc1df589e784821a1ba89e17
Author: Caideyipi <87...@users.noreply.github.com>
AuthorDate: Tue May 12 17:11:44 2026 +0800

    Load: Harden LOAD TSFILE source path validation (#17624)
    
    * Load pri
    
    * sp
    
    * MAINTAIN
    
    * rollback
    
    * Add
    
    * change
    
    * canonical
    
    * line
    
    * Pre
---
 .../java/org/apache/iotdb/db/conf/IoTDBConfig.java |  83 ++++++++++++
 .../org/apache/iotdb/db/conf/IoTDBDescriptor.java  |  28 +++++
 .../protocol/legacy/loader/TsFileLoader.java       |   2 +-
 .../protocol/thrift/IoTDBDataNodeReceiver.java     |   3 +-
 .../plan/analyze/load/LoadTsFileAnalyzer.java      |   2 +-
 .../plan/scheduler/load/LoadTsFileScheduler.java   |   2 +-
 .../plan/statement/crud/LoadTsFileStatement.java   |  70 ++++++++++-
 .../load/active/ActiveLoadTsFileLoader.java        |   3 +-
 .../statement/crud/LoadTsFileStatementTest.java    | 139 +++++++++++++++++++++
 .../conf/iotdb-system.properties.template          |  11 ++
 10 files changed, 333 insertions(+), 10 deletions(-)

diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
index d3f13d14392..07c73b43d87 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
@@ -61,8 +61,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -324,6 +326,14 @@ public class IoTDBConfig {
     tierDataDirs[0][0] + File.separator + IoTDBConstant.LOAD_TSFILE_FOLDER_NAME
   };
 
+  private String[] loadTsFileAllowedDirs = new String[0];
+
+  private CanonicalPaths loadTsFileDirCanonicalPaths = canonicalPaths(loadTsFileDirs);
+
+  private CanonicalPaths loadTsFileAllowedDirCanonicalPaths = canonicalPaths(loadTsFileAllowedDirs);
+
+  private boolean loadTsFileSourcePathCheckEnabled = false;
+
   /** Strategy of multiple directories. */
   private String multiDirStrategyClassName = null;
 
@@ -1394,6 +1404,10 @@ public class IoTDBConfig {
     for (int i = 0; i < loadActiveListeningDirs.length; i++) {
       loadActiveListeningDirs[i] = addDataHomeDir(loadActiveListeningDirs[i]);
     }
+    for (int i = 0; i < loadTsFileAllowedDirs.length; i++) {
+      loadTsFileAllowedDirs[i] = addDataHomeDir(loadTsFileAllowedDirs[i]);
+    }
+    loadTsFileAllowedDirCanonicalPaths = canonicalPaths(loadTsFileAllowedDirs);
     loadActiveListeningPipeDir = addDataHomeDir(loadActiveListeningPipeDir);
     loadActiveListeningFailDir = addDataHomeDir(loadActiveListeningFailDir);
     udfDir = addDataHomeDir(udfDir);
@@ -1589,6 +1603,36 @@ public class IoTDBConfig {
     return this.loadTsFileDirs;
   }
 
+  public String[] getLoadTsFileAllowedDirs() {
+    return this.loadTsFileAllowedDirs.length == 0
+        ? getLoadTsFileDirs()
+        : this.loadTsFileAllowedDirs;
+  }
+
+  public Path[] getLoadTsFileAllowedDirCanonicalPaths() throws FileNotFoundException {
+    return (this.loadTsFileAllowedDirs.length == 0
+            ? this.loadTsFileDirCanonicalPaths
+            : this.loadTsFileAllowedDirCanonicalPaths)
+        .getPaths();
+  }
+
+  public boolean isLoadTsFileSourcePathCheckEnabled() {
+    return loadTsFileSourcePathCheckEnabled;
+  }
+
+  public void setLoadTsFileSourcePathCheckEnabled(boolean loadTsFileSourcePathCheckEnabled) {
+    this.loadTsFileSourcePathCheckEnabled = loadTsFileSourcePathCheckEnabled;
+  }
+
+  public void setLoadTsFileAllowedDirs(String[] loadTsFileAllowedDirs) {
+    final String[] newLoadTsFileAllowedDirs = new String[loadTsFileAllowedDirs.length];
+    for (int i = 0; i < loadTsFileAllowedDirs.length; i++) {
+      newLoadTsFileAllowedDirs[i] = addDataHomeDir(loadTsFileAllowedDirs[i]);
+    }
+    this.loadTsFileAllowedDirs = newLoadTsFileAllowedDirs;
+    this.loadTsFileAllowedDirCanonicalPaths = canonicalPaths(newLoadTsFileAllowedDirs);
+  }
+
   public void formulateLoadTsFileDirs(String[][] tierDataDirs) {
     if (tierDataDirs.length < 1) {
       logger.warn("No data directory is set. loadTsFileDirs is kept as the default value.");
@@ -1606,6 +1650,45 @@ public class IoTDBConfig {
     // or the newLoadTsFileDirs will be used in the middle of the process
     // and cause the undefined behavior.
     this.loadTsFileDirs = newLoadTsFileDirs;
+    this.loadTsFileDirCanonicalPaths = canonicalPaths(newLoadTsFileDirs);
+  }
+
+  private static CanonicalPaths canonicalPaths(final String[] dirs) {
+    final Path[] paths = new Path[dirs.length];
+    for (int i = 0; i < dirs.length; i++) {
+      try {
+        paths[i] = new File(dirs[i]).getCanonicalFile().toPath();
+      } catch (final IOException e) {
+        return new CanonicalPaths(
+            String.format(
+                "Failed to resolve canonical path for Load TsFile allowed directory %s: %s",
+                dirs[i], e.getMessage()));
+      }
+    }
+    return new CanonicalPaths(paths);
+  }
+
+  private static class CanonicalPaths {
+
+    private final Path[] paths;
+    private final String errorMessage;
+
+    private CanonicalPaths(final Path[] paths) {
+      this.paths = paths;
+      this.errorMessage = null;
+    }
+
+    private CanonicalPaths(final String errorMessage) {
+      this.paths = new Path[0];
+      this.errorMessage = errorMessage;
+    }
+
+    private Path[] getPaths() throws FileNotFoundException {
+      if (errorMessage != null) {
+        throw new FileNotFoundException(errorMessage);
+      }
+      return paths;
+    }
   }
 
   public String getSchemaDir() {
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
index b01fca1a88f..3cec197ac0f 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
@@ -2449,6 +2449,17 @@ public class IoTDBDescriptor {
             properties.getProperty(
                 "load_write_throughput_bytes_per_second",
                 String.valueOf(conf.getLoadWriteThroughputBytesPerSecond()))));
+
+    conf.setLoadTsFileAllowedDirs(
+        Arrays.stream(properties.getProperty("load_tsfile_allowed_dirs", "").trim().split(","))
+            .filter(dir -> !dir.isEmpty())
+            .toArray(String[]::new));
+    conf.setLoadTsFileSourcePathCheckEnabled(
+        Boolean.parseBoolean(
+            properties.getProperty(
+                "load_tsfile_source_path_check_enable",
+                Boolean.toString(conf.isLoadTsFileSourcePathCheckEnabled()))));
+
     conf.setLoadTabletConversionThresholdBytes(
         Long.parseLong(
             properties.getProperty(
@@ -2560,6 +2571,23 @@ public class IoTDBDescriptor {
                 ConfigurationFileUtils.getConfigurationDefaultValue(
                     "load_write_throughput_bytes_per_second"))));
 
+    conf.setLoadTsFileAllowedDirs(
+        Arrays.stream(
+                properties
+                    .getProperty(
+                        "load_tsfile_allowed_dirs",
+                        ConfigurationFileUtils.getConfigurationDefaultValue(
+                            "load_tsfile_allowed_dirs"))
+                    .trim()
+                    .split(","))
+            .filter(dir -> !dir.isEmpty())
+            .toArray(String[]::new));
+    conf.setLoadTsFileSourcePathCheckEnabled(
+        Boolean.parseBoolean(
+            properties.getProperty(
+                "load_tsfile_source_path_check_enable",
+                Boolean.toString(conf.isLoadTsFileSourcePathCheckEnabled()))));
+
     conf.setLoadActiveListeningEnable(
         Boolean.parseBoolean(
             properties.getProperty(
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/legacy/loader/TsFileLoader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/legacy/loader/TsFileLoader.java
index f88510a0ca8..8459e22f1ee 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/legacy/loader/TsFileLoader.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/legacy/loader/TsFileLoader.java
@@ -54,7 +54,7 @@ public class TsFileLoader implements ILoader {
   @Override
   public void load() {
     try {
-      LoadTsFileStatement statement = new LoadTsFileStatement(tsFile.getAbsolutePath());
+      LoadTsFileStatement statement = LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath());
       statement.setDeleteAfterLoad(true);
       statement.setConvertOnTypeMismatch(true);
       statement.setDatabaseLevel(parseSgLevel());
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
index 334c3530414..58d4c29eddc 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
@@ -490,8 +490,7 @@ public class IoTDBDataNodeReceiver extends IoTDBFileReceiver {
   }
 
   private TSStatus loadTsFileSync(final String fileAbsolutePath) throws FileNotFoundException {
-    final LoadTsFileStatement statement = new LoadTsFileStatement(fileAbsolutePath);
-
+    final LoadTsFileStatement statement = LoadTsFileStatement.createUnchecked(fileAbsolutePath);
     statement.setDeleteAfterLoad(true);
     statement.setConvertOnTypeMismatch(true);
     statement.setVerifySchema(validateTsFile.get());
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
index 5a346fdf8e0..b6d4fb0043b 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
@@ -372,7 +372,7 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
       final TSStatus status =
           loadTsFileDataTypeConverter
               .convertForTreeModel(
-                  new LoadTsFileStatement(tsFiles.get(i).getPath())
+                  LoadTsFileStatement.createUnchecked(tsFiles.get(i).getPath())
                       .setDeleteAfterLoad(isDeleteAfterLoad)
                       .setConvertOnTypeMismatch(isConvertOnTypeMismatch))
               .orElse(null);
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
index 4a85b6d0ebc..5cafe3b8384 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
@@ -551,7 +551,7 @@ public class LoadTsFileScheduler implements IScheduler {
         final TSStatus status =
             loadTsFileDataTypeConverter
                 .convertForTreeModel(
-                    new LoadTsFileStatement(filePath)
+                    LoadTsFileStatement.createUnchecked(filePath)
                         .setDeleteAfterLoad(failedNode.isDeleteAfterLoad())
                         .setConvertOnTypeMismatch(true))
                 .orElse(null);
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatement.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatement.java
index 3b10d3733e1..6e74ceed206 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatement.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatement.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.db.queryengine.plan.statement.crud;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.db.conf.IoTDBConfig;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.queryengine.plan.statement.Statement;
 import org.apache.iotdb.db.queryengine.plan.statement.StatementType;
@@ -34,7 +35,9 @@ import org.apache.tsfile.common.constant.TsFileConstant;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -56,6 +59,15 @@ public class LoadTsFileStatement extends Statement {
   private List<Long> writePointCountList;
 
   public LoadTsFileStatement(String filePath) throws FileNotFoundException {
+    this(filePath, true);
+  }
+
+  public static LoadTsFileStatement createUnchecked(String filePath) throws FileNotFoundException {
+    return new LoadTsFileStatement(filePath, false);
+  }
+
+  private LoadTsFileStatement(String filePath, boolean validateSourcePath)
+      throws FileNotFoundException {
     this.file = new File(filePath).getAbsoluteFile();
     this.databaseLevel = IoTDBDescriptor.getInstance().getConfig().getDefaultDatabaseLevel();
     this.verifySchema = true;
@@ -65,13 +77,22 @@ public class LoadTsFileStatement extends Statement {
         IoTDBDescriptor.getInstance().getConfig().getLoadTabletConversionThresholdBytes();
     this.autoCreateDatabase = IoTDBDescriptor.getInstance().getConfig().isAutoCreateSchemaEnabled();
 
-    this.tsFiles = processTsFile(file);
+    this.tsFiles = processTsFile(file, validateSourcePath);
     this.resources = new ArrayList<>();
     this.writePointCountList = new ArrayList<>();
     this.statementType = StatementType.MULTI_BATCH_INSERT;
   }
 
   public static List<File> processTsFile(final File file) throws FileNotFoundException {
+    return processTsFile(file, true);
+  }
+
+  public static List<File> processTsFile(final File file, final boolean validateSourcePath)
+      throws FileNotFoundException {
+    if (validateSourcePath) {
+      validateLoadSourcePath(file);
+    }
+
     final List<File> tsFiles = new ArrayList<>();
     if (file.isFile()) {
       tsFiles.add(file);
@@ -82,7 +103,7 @@ public class LoadTsFileStatement extends Statement {
                 "Can not find %s on this machine, notice that load can only handle files on this machine.",
                 file.getPath()));
       }
-      tsFiles.addAll(findAllTsFile(file));
+      tsFiles.addAll(findAllTsFile(file, validateSourcePath));
     }
     sortTsFiles(tsFiles);
     return tsFiles;
@@ -101,7 +122,8 @@ public class LoadTsFileStatement extends Statement {
     this.statementType = StatementType.MULTI_BATCH_INSERT;
   }
 
-  private static List<File> findAllTsFile(File file) {
+  private static List<File> findAllTsFile(File file, boolean validateSourcePath)
+      throws FileNotFoundException {
     final File[] files = file.listFiles();
     if (files == null) {
       return Collections.emptyList();
@@ -109,15 +131,55 @@ public class LoadTsFileStatement extends Statement {
 
     final List<File> tsFiles = new ArrayList<>();
     for (File nowFile : files) {
+      if (validateSourcePath) {
+        validateLoadSourcePath(nowFile);
+      }
       if (nowFile.getName().endsWith(TsFileConstant.TSFILE_SUFFIX)) {
         tsFiles.add(nowFile);
       } else if (nowFile.isDirectory()) {
-        tsFiles.addAll(findAllTsFile(nowFile));
+        tsFiles.addAll(findAllTsFile(nowFile, validateSourcePath));
       }
     }
     return tsFiles;
   }
 
+  public static void validateLoadSourcePath(final String filePath) throws FileNotFoundException {
+    validateLoadSourcePath(new File(filePath));
+  }
+
+  private static void validateLoadSourcePath(final File file) throws FileNotFoundException {
+    final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+    if (!config.isLoadTsFileSourcePathCheckEnabled()) {
+      return;
+    }
+
+    final Path sourcePath = canonicalPath(file);
+    final String[] allowedDirs = config.getLoadTsFileAllowedDirs();
+    final Path[] allowedDirCanonicalPaths = config.getLoadTsFileAllowedDirCanonicalPaths();
+
+    for (final Path allowedDirCanonicalPath : allowedDirCanonicalPaths) {
+      if (sourcePath.startsWith(allowedDirCanonicalPath)) {
+        return;
+      }
+    }
+
+    throw new FileNotFoundException(
+        String.format(
+            "Load TsFile source path %s is outside allowed directories %s.",
+            sourcePath, Arrays.toString(allowedDirs)));
+  }
+
+  private static Path canonicalPath(final File file) throws FileNotFoundException {
+    try {
+      return file.getCanonicalFile().toPath();
+    } catch (final IOException e) {
+      throw new FileNotFoundException(
+          String.format(
+              "Failed to resolve canonical path for Load TsFile source %s: %s",
+              file.getPath(), e.getMessage()));
+    }
+  }
+
   private static void sortTsFiles(List<File> files) {
     files.sort(
         (o1, o2) -> {
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
index 8a1f305e4f2..a228ef6c651 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
@@ -218,7 +218,8 @@ public class ActiveLoadTsFileLoader {
       final ActiveLoadPendingQueue.ActiveLoadEntry entry, final IClientSession session)
       throws FileNotFoundException {
     final File tsFile = new File(entry.getFile());
-    final LoadTsFileStatement statement = new LoadTsFileStatement(entry.getFile());
+    final LoadTsFileStatement statement =
+        LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath());
 
     statement.setDeleteAfterLoad(true);
     statement.setAutoCreateDatabase(
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
new file mode 100644
index 00000000000..bfebf51d281
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/statement/crud/LoadTsFileStatementTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.queryengine.plan.statement.crud;
+
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Stream;
+
+public class LoadTsFileStatementTest {
+
+  @Test
+  public void testSubStatementsKeepDatabase() throws Exception {
+    final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+    final int originalBatchSize = config.getLoadTsFileSubStatementBatchSize();
+    final String[] originalAllowedDirs = config.getLoadTsFileAllowedDirs().clone();
+    final Path tempDir = Files.createTempDirectory("load-tsfile-sub-statements");
+
+    try {
+      config.setLoadTsFileSubStatementBatchSize(1);
+      config.setLoadTsFileAllowedDirs(new String[] {tempDir.toString()});
+      Files.createFile(tempDir.resolve("a.tsfile"));
+      Files.createFile(tempDir.resolve("b.tsfile"));
+
+      final LoadTsFileStatement statement = new LoadTsFileStatement(tempDir.toString());
+      statement.setDatabase("test_db");
+
+      final List<LoadTsFileStatement> subStatements = statement.getSubStatements();
+      Assert.assertEquals(2, subStatements.size());
+      subStatements.forEach(
+          subStatement -> Assert.assertEquals("test_db", subStatement.getDatabase()));
+    } finally {
+      config.setLoadTsFileSubStatementBatchSize(originalBatchSize);
+      config.setLoadTsFileAllowedDirs(originalAllowedDirs);
+      deleteRecursively(tempDir);
+    }
+  }
+
+  @Test
+  public void testLoadSourcePathMustBeInAllowedDirs() throws Exception {
+    final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+    final String[] originalAllowedDirs = config.getLoadTsFileAllowedDirs().clone();
+    final boolean originalCheckEnabled = config.isLoadTsFileSourcePathCheckEnabled();
+    final Path allowedDir = Files.createTempDirectory("load-tsfile-allowed");
+    final Path deniedDir = Files.createTempDirectory("load-tsfile-denied");
+
+    try {
+      config.setLoadTsFileSourcePathCheckEnabled(true);
+      config.setLoadTsFileAllowedDirs(new String[] {allowedDir.toString()});
+      final Path deniedTsFile = Files.createFile(deniedDir.resolve("denied.tsfile"));
+      final Path traversalTsFile =
+          allowedDir.resolve("..").resolve(deniedDir.getFileName()).resolve("denied.tsfile");
+
+      assertLoadSourcePathRejected(deniedTsFile);
+      assertLoadSourcePathRejected(traversalTsFile);
+    } finally {
+      config.setLoadTsFileAllowedDirs(originalAllowedDirs);
+      config.setLoadTsFileSourcePathCheckEnabled(originalCheckEnabled);
+      deleteRecursively(allowedDir);
+      deleteRecursively(deniedDir);
+    }
+  }
+
+  @Test
+  public void testLoadSourcePathCheckCanBeDisabled() throws Exception {
+    final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+    final String[] originalAllowedDirs = config.getLoadTsFileAllowedDirs().clone();
+    final boolean originalCheckEnabled = config.isLoadTsFileSourcePathCheckEnabled();
+    final Path allowedDir = Files.createTempDirectory("load-tsfile-allowed");
+    final Path deniedDir = Files.createTempDirectory("load-tsfile-denied");
+
+    try {
+      config.setLoadTsFileSourcePathCheckEnabled(false);
+      config.setLoadTsFileAllowedDirs(new String[] {allowedDir.toString()});
+      final Path deniedTsFile = Files.createFile(deniedDir.resolve("denied.tsfile"));
+
+      new LoadTsFileStatement(deniedTsFile.toString());
+    } finally {
+      config.setLoadTsFileAllowedDirs(originalAllowedDirs);
+      config.setLoadTsFileSourcePathCheckEnabled(originalCheckEnabled);
+      deleteRecursively(allowedDir);
+      deleteRecursively(deniedDir);
+    }
+  }
+
+  private static void assertLoadSourcePathRejected(final Path sourcePath) {
+    try {
+      new LoadTsFileStatement(sourcePath.toString());
+      Assert.fail("Expected disallowed LOAD TSFILE source path to be rejected.");
+    } catch (final FileNotFoundException e) {
+      Assert.assertTrue(e.getMessage().contains("outside allowed directories"));
+    }
+  }
+
+  private static void deleteRecursively(final Path path) throws IOException {
+    if (path == null || !Files.exists(path)) {
+      return;
+    }
+
+    try (final Stream<Path> pathStream = Files.walk(path)) {
+      pathStream
+          .sorted(Comparator.reverseOrder())
+          .forEach(
+              currentPath -> {
+                try {
+                  Files.deleteIfExists(currentPath);
+                } catch (IOException e) {
+                  throw new RuntimeException(e);
+                }
+              });
+    }
+  }
+}
diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index 5447bcf5fd4..741a4c846cd 100644
--- a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -2033,6 +2033,17 @@ load_clean_up_task_execution_delay_time_seconds=1800
 # Datatype: int
 load_write_throughput_bytes_per_second=-1
 
+# Whether the load_tsfile supports path allowed dirs check.
+# effectiveMode: hot_reload
+# Datatype: String
+load_tsfile_source_path_check_enable=false
+
+# Comma-separated list of directories from which user-issued LOAD TSFILE statements can read.
+# If empty, IoTDB only permits LOAD sources under the internal load TsFile directories.
+# effectiveMode: hot_reload
+# Datatype: String
+load_tsfile_allowed_dirs=
+
 # Whether to enable the active listening mode for tsfile loading.
 # effectiveMode: hot_reload
 # Datatype: Boolean