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;
+    }
+  }
+}

Reply via email to