This is an automated email from the ASF dual-hosted git repository.
jiangtian 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 57fe1c9a48e Pipe: check file receiver write path (#17442)
57fe1c9a48e is described below
commit 57fe1c9a48e8ff9c0f7a6565583dacb5f1025d8f
Author: Zhenyu Luo <[email protected]>
AuthorDate: Thu Apr 9 15:26:53 2026 +0800
Pipe: check file receiver write path (#17442)
* Pipe: prevent path traversal in file receiver write path
Normalize and validate incoming file paths against the receiver base
directory before creating write targets, preventing directory-escape writes and
strengthening receiver-side file safety.
Made-with: Cursor
* update
---
.../commons/pipe/receiver/IoTDBFileReceiver.java | 13 +-
.../pipe/receiver/IoTDBFileReceiverTest.java | 133 +++++++++++++++++++++
2 files changed, 145 insertions(+), 1 deletion(-)
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
index ff8c8dbd293..1105a21d8ac 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
@@ -50,6 +50,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -496,8 +497,18 @@ public abstract class IoTDBFileReceiver implements
IoTDBReceiver {
receiverFileDirWithIdSuffix.get().getPath());
}
}
+ Path baseDir =
receiverFileDirWithIdSuffix.get().toPath().toAbsolutePath().normalize();
+ Path targetPath = baseDir.resolve(fileName).toAbsolutePath().normalize();
- writingFile = new File(receiverFileDirWithIdSuffix.get(), fileName);
+ if (!targetPath.startsWith(baseDir)) {
+ LOGGER.error(
+ "Receiver id = {}: Path traversal attempt detected! Filename: {}",
+ receiverId.get(),
+ fileName);
+ throw new IOException("Illegal fileName: " + fileName + " (Path
traversal detected)");
+ }
+
+ writingFile = targetPath.toFile();
writingFileWriter = new RandomAccessFile(writingFile, "rw");
LOGGER.info(
"Receiver id = {}: Writing file {} was created. Ready to write file
pieces.",
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
new file mode 100644
index 00000000000..a372326433d
--- /dev/null
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.commons.pipe.receiver;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV1;
+import
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV2;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferResp;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public class IoTDBFileReceiverTest {
+
+ @Test
+ public void testRejectPathTraversalFileName() throws Exception {
+ final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
+ final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
+ try {
+ final IOException exception =
+ Assert.assertThrows(
+ IOException.class, () ->
receiver.createWritingFile("../outside.tsfile", true));
+ Assert.assertTrue(exception.getMessage().contains("Illegal fileName"));
+ } finally {
+ receiver.handleExit();
+ }
+ }
+
+ @Test
+ public void testAllowNormalFileName() throws Exception {
+ final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
+ final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
+ try {
+ receiver.createWritingFile("normal.tsfile", true);
+
Assert.assertTrue(receiver.getWritingFileInBaseDir("normal.tsfile").exists());
+ } finally {
+ receiver.handleExit();
+ }
+ }
+
+ private static class DummyFileReceiver extends IoTDBFileReceiver {
+
+ DummyFileReceiver(final File baseDir) {
+ receiverFileDirWithIdSuffix.set(baseDir);
+ }
+
+ void createWritingFile(final String fileName, final boolean isSingleFile)
throws IOException {
+ updateWritingFileIfNeeded(fileName, isSingleFile);
+ }
+
+ File getWritingFileInBaseDir(final String fileName) {
+ return
receiverFileDirWithIdSuffix.get().toPath().resolve(fileName).toFile();
+ }
+
+ @Override
+ protected String getReceiverFileBaseDir() {
+ return receiverFileDirWithIdSuffix.get().getAbsolutePath();
+ }
+
+ @Override
+ protected void markFileBaseDirStateAbnormal(final String dir) {
+ // noop for unit test
+ }
+
+ @Override
+ protected String getSenderHost() {
+ return "127.0.0.1";
+ }
+
+ @Override
+ protected String getSenderPort() {
+ return "6667";
+ }
+
+ @Override
+ protected String getClusterId() {
+ return "test-cluster";
+ }
+
+ @Override
+ protected TSStatus login() {
+ return new TSStatus(200);
+ }
+
+ @Override
+ protected TSStatus loadFileV1(
+ final PipeTransferFileSealReqV1 req, final String fileAbsolutePath) {
+ return new TSStatus(200);
+ }
+
+ @Override
+ protected TSStatus loadFileV2(
+ final PipeTransferFileSealReqV2 req, final List<String>
fileAbsolutePaths)
+ throws IllegalPathException {
+ return new TSStatus(200);
+ }
+
+ @Override
+ protected void closeSession() {
+ // noop for unit test
+ }
+
+ @Override
+ public TPipeTransferResp receive(TPipeTransferReq req) {
+ return null;
+ }
+ }
+}