From 2e29ecde896514a3c3ac86033833aa822db95d10 Mon Sep 17 00:00:00 2001 From: luoluoyuyu Date: Wed, 8 Apr 2026 11:51:45 +0800 Subject: [PATCH 1/2] 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 --- .../commons/pipe/receiver/IoTDBFileReceiver.java | 13 ++++++++++++- 1 file changed, 12 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 ff8c8dbd293d8..1105a21d8acd3 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 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 @@ protected final void updateWritingFileIfNeeded(final String fileName, final bool 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.", From 8eeae3131b837aa233005cdc58f3980d629918aa Mon Sep 17 00:00:00 2001 From: luoluoyuyu Date: Wed, 8 Apr 2026 17:55:46 +0800 Subject: [PATCH 2/2] update --- .../pipe/receiver/IoTDBFileReceiverTest.java | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java 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 0000000000000..a372326433d6b --- /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 fileAbsolutePaths) + throws IllegalPathException { + return new TSStatus(200); + } + + @Override + protected void closeSession() { + // noop for unit test + } + + @Override + public TPipeTransferResp receive(TPipeTransferReq req) { + return null; + } + } +}