This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 5029a0aa88e Load: Harden LOAD TSFILE source path validation (#17624)
5029a0aa88e is described below
commit 5029a0aa88e0e0022e26de963ee55f521724676a
Author: Caideyipi <[email protected]>
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 | 27 +++++++
.../protocol/legacy/loader/TsFileLoader.java | 2 +-
.../protocol/thrift/IoTDBDataNodeReceiver.java | 2 +-
.../plan/analyze/load/LoadTsFileAnalyzer.java | 10 +--
.../plan/relational/sql/ast/LoadTsFile.java | 17 ++++-
.../plan/scheduler/load/LoadTsFileScheduler.java | 4 +-
.../plan/statement/crud/LoadTsFileStatement.java | 72 +++++++++++++++++--
.../load/active/ActiveLoadTsFileLoader.java | 3 +-
.../statement/crud/LoadTsFileStatementTest.java | 60 ++++++++++++++++
.../conf/iotdb-system.properties.template | 11 +++
11 files changed, 275 insertions(+), 16 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 9b177cffcfa..3e906f0cdbe 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
@@ -60,8 +60,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.Collections;
@@ -298,6 +300,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;
@@ -1355,6 +1365,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);
@@ -1560,6 +1574,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.");
@@ -1577,6 +1621,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 7193fd27c58..abfe4ec41d9 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
@@ -2456,6 +2456,16 @@ public class IoTDBDescriptor {
"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(
@@ -2573,6 +2583,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 1c95b574b54..92139942b0c 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
@@ -55,7 +55,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 c10fdbc4f67..dd6e684031c 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
@@ -585,7 +585,7 @@ public class IoTDBDataNodeReceiver extends
IoTDBFileReceiver {
private TSStatus loadTsFileSync(final String dataBaseName, 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 9dd8ad4eb02..f55b6172f2c 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
@@ -443,14 +443,15 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
isTableModelTsFile.get(i)
? loadTsFileDataTypeConverter
.convertForTableModel(
- new LoadTsFile(null, tsFiles.get(i).getPath(),
Collections.emptyMap())
+ LoadTsFile.createUnchecked(
+ null, tsFiles.get(i).getPath(),
Collections.emptyMap())
.setDatabase(databaseForTableData)
.setDeleteAfterLoad(isDeleteAfterLoad)
.setConvertOnTypeMismatch(isConvertOnTypeMismatch))
.orElse(null)
: loadTsFileDataTypeConverter
.convertForTreeModel(
- new LoadTsFileStatement(tsFiles.get(i).getPath())
+
LoadTsFileStatement.createUnchecked(tsFiles.get(i).getPath())
.setDeleteAfterLoad(isDeleteAfterLoad)
.setConvertOnTypeMismatch(isConvertOnTypeMismatch))
.orElse(null);
@@ -712,14 +713,15 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
isTableModelTsFile.get(i)
? loadTsFileDataTypeConverter
.convertForTableModel(
- new LoadTsFile(null, tsFiles.get(i).getPath(),
Collections.emptyMap())
+ LoadTsFile.createUnchecked(
+ null, tsFiles.get(i).getPath(),
Collections.emptyMap())
.setDatabase(databaseForTableData)
.setDeleteAfterLoad(isDeleteAfterLoad)
.setConvertOnTypeMismatch(isConvertOnTypeMismatch))
.orElse(null)
: 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/relational/sql/ast/LoadTsFile.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/LoadTsFile.java
index 03de9a9ccb1..dc1a549f636 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/LoadTsFile.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/LoadTsFile.java
@@ -72,6 +72,19 @@ public class LoadTsFile extends Statement {
private boolean needDecode4TimeColumn;
public LoadTsFile(NodeLocation location, String filePath, Map<String,
String> loadAttributes) {
+ this(location, filePath, loadAttributes, true);
+ }
+
+ public static LoadTsFile createUnchecked(
+ NodeLocation location, String filePath, Map<String, String>
loadAttributes) {
+ return new LoadTsFile(location, filePath, loadAttributes, false);
+ }
+
+ private LoadTsFile(
+ NodeLocation location,
+ String filePath,
+ Map<String, String> loadAttributes,
+ boolean validateSourcePath) {
super(location);
this.filePath = requireNonNull(filePath, "filePath is null");
@@ -89,7 +102,7 @@ public class LoadTsFile extends Statement {
try {
this.tsFiles =
org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement.processTsFile(
- new File(filePath));
+ new File(filePath), validateSourcePath);
this.resources = new ArrayList<>();
this.writePointCountList = new ArrayList<>();
this.isTableModel = new
ArrayList<>(Collections.nCopies(this.tsFiles.size(), true));
@@ -283,7 +296,7 @@ public class LoadTsFile extends Statement {
final Map<String, String> properties = this.loadAttributes;
final LoadTsFile subStatement =
- new LoadTsFile(getLocation().orElse(null), filePath, properties);
+ LoadTsFile.createUnchecked(getLocation().orElse(null), filePath,
properties);
// Copy all configuration properties
subStatement.databaseLevel = this.databaseLevel;
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 bc8446fd3da..6671dad3548 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
@@ -586,14 +586,14 @@ public class LoadTsFileScheduler implements IScheduler {
failedNode.isTableModel()
? loadTsFileDataTypeConverter
.convertForTableModel(
- new LoadTsFile(null, filePath, Collections.emptyMap())
+ LoadTsFile.createUnchecked(null, filePath,
Collections.emptyMap())
.setDatabase(failedNode.getDatabase())
.setDeleteAfterLoad(failedNode.isDeleteAfterLoad())
.setConvertOnTypeMismatch(true))
.orElse(null)
: 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 512a62fdae7..59c7f9a57ef 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
@@ -20,6 +20,7 @@
package org.apache.iotdb.db.queryengine.plan.statement.crud;
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.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.LoadTsFile;
@@ -35,7 +36,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.HashMap;
import java.util.List;
@@ -71,6 +74,15 @@ public class LoadTsFileStatement extends Statement {
private boolean needDecode4TimeColumn;
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;
@@ -80,7 +92,7 @@ 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.isTableModel = new
ArrayList<>(Collections.nCopies(this.tsFiles.size(), false));
@@ -88,6 +100,15 @@ public class LoadTsFileStatement extends Statement {
}
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);
@@ -98,7 +119,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;
@@ -120,7 +141,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();
@@ -128,15 +150,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) -> {
@@ -389,7 +451,7 @@ public class LoadTsFileStatement extends Statement {
loadAttributes.put(PIPE_GENERATED_KEY, String.valueOf(true));
}
- return new LoadTsFile(null, file.getAbsolutePath(), loadAttributes);
+ return LoadTsFile.createUnchecked(null, file.getAbsolutePath(),
loadAttributes);
}
@Override
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 d0be2ead5cb..67fa0fff300 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
@@ -219,7 +219,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(tsFile.getAbsolutePath());
+ final LoadTsFileStatement statement =
+ LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath());
final List<File> files = statement.getTsFiles();
statement.setDeleteAfterLoad(true);
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 941794bb074..bfebf51d281 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
@@ -25,6 +25,7 @@ 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;
@@ -38,10 +39,12 @@ public class LoadTsFileStatementTest {
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"));
@@ -54,10 +57,67 @@ public class LoadTsFileStatementTest {
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;
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 69bcd275250..623dffee28b 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
@@ -2182,6 +2182,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