Skip to content

Commit 57fe1c9

Browse files
authored
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
1 parent 37fb889 commit 57fe1c9

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.io.File;
5151
import java.io.IOException;
5252
import java.io.RandomAccessFile;
53+
import java.nio.file.Path;
5354
import java.util.List;
5455
import java.util.Objects;
5556
import java.util.concurrent.atomic.AtomicBoolean;
@@ -496,8 +497,18 @@ protected final void updateWritingFileIfNeeded(final String fileName, final bool
496497
receiverFileDirWithIdSuffix.get().getPath());
497498
}
498499
}
500+
Path baseDir = receiverFileDirWithIdSuffix.get().toPath().toAbsolutePath().normalize();
501+
Path targetPath = baseDir.resolve(fileName).toAbsolutePath().normalize();
499502

500-
writingFile = new File(receiverFileDirWithIdSuffix.get(), fileName);
503+
if (!targetPath.startsWith(baseDir)) {
504+
LOGGER.error(
505+
"Receiver id = {}: Path traversal attempt detected! Filename: {}",
506+
receiverId.get(),
507+
fileName);
508+
throw new IOException("Illegal fileName: " + fileName + " (Path traversal detected)");
509+
}
510+
511+
writingFile = targetPath.toFile();
501512
writingFileWriter = new RandomAccessFile(writingFile, "rw");
502513
LOGGER.info(
503514
"Receiver id = {}: Writing file {} was created. Ready to write file pieces.",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.commons.pipe.receiver;
21+
22+
import org.apache.iotdb.common.rpc.thrift.TSStatus;
23+
import org.apache.iotdb.commons.exception.IllegalPathException;
24+
import org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV1;
25+
import org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV2;
26+
import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
27+
import org.apache.iotdb.service.rpc.thrift.TPipeTransferResp;
28+
29+
import org.junit.Assert;
30+
import org.junit.Test;
31+
32+
import java.io.File;
33+
import java.io.IOException;
34+
import java.nio.file.Files;
35+
import java.nio.file.Path;
36+
import java.util.List;
37+
38+
public class IoTDBFileReceiverTest {
39+
40+
@Test
41+
public void testRejectPathTraversalFileName() throws Exception {
42+
final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
43+
final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
44+
try {
45+
final IOException exception =
46+
Assert.assertThrows(
47+
IOException.class, () -> receiver.createWritingFile("../outside.tsfile", true));
48+
Assert.assertTrue(exception.getMessage().contains("Illegal fileName"));
49+
} finally {
50+
receiver.handleExit();
51+
}
52+
}
53+
54+
@Test
55+
public void testAllowNormalFileName() throws Exception {
56+
final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
57+
final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
58+
try {
59+
receiver.createWritingFile("normal.tsfile", true);
60+
Assert.assertTrue(receiver.getWritingFileInBaseDir("normal.tsfile").exists());
61+
} finally {
62+
receiver.handleExit();
63+
}
64+
}
65+
66+
private static class DummyFileReceiver extends IoTDBFileReceiver {
67+
68+
DummyFileReceiver(final File baseDir) {
69+
receiverFileDirWithIdSuffix.set(baseDir);
70+
}
71+
72+
void createWritingFile(final String fileName, final boolean isSingleFile) throws IOException {
73+
updateWritingFileIfNeeded(fileName, isSingleFile);
74+
}
75+
76+
File getWritingFileInBaseDir(final String fileName) {
77+
return receiverFileDirWithIdSuffix.get().toPath().resolve(fileName).toFile();
78+
}
79+
80+
@Override
81+
protected String getReceiverFileBaseDir() {
82+
return receiverFileDirWithIdSuffix.get().getAbsolutePath();
83+
}
84+
85+
@Override
86+
protected void markFileBaseDirStateAbnormal(final String dir) {
87+
// noop for unit test
88+
}
89+
90+
@Override
91+
protected String getSenderHost() {
92+
return "127.0.0.1";
93+
}
94+
95+
@Override
96+
protected String getSenderPort() {
97+
return "6667";
98+
}
99+
100+
@Override
101+
protected String getClusterId() {
102+
return "test-cluster";
103+
}
104+
105+
@Override
106+
protected TSStatus login() {
107+
return new TSStatus(200);
108+
}
109+
110+
@Override
111+
protected TSStatus loadFileV1(
112+
final PipeTransferFileSealReqV1 req, final String fileAbsolutePath) {
113+
return new TSStatus(200);
114+
}
115+
116+
@Override
117+
protected TSStatus loadFileV2(
118+
final PipeTransferFileSealReqV2 req, final List<String> fileAbsolutePaths)
119+
throws IllegalPathException {
120+
return new TSStatus(200);
121+
}
122+
123+
@Override
124+
protected void closeSession() {
125+
// noop for unit test
126+
}
127+
128+
@Override
129+
public TPipeTransferResp receive(TPipeTransferReq req) {
130+
return null;
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)