Skip to content

Commit b8f33a6

Browse files
authored
[To dev/1.3] Pipe: check file receiver write path (#17442) (#17501)
* 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 (cherry picked from commit 57fe1c9) * spotless
1 parent 8bf1640 commit b8f33a6

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

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
@@ -48,6 +48,7 @@
4848
import java.io.File;
4949
import java.io.IOException;
5050
import java.io.RandomAccessFile;
51+
import java.nio.file.Path;
5152
import java.util.List;
5253
import java.util.Objects;
5354
import java.util.concurrent.atomic.AtomicBoolean;
@@ -460,8 +461,18 @@ protected final void updateWritingFileIfNeeded(final String fileName, final bool
460461
receiverFileDirWithIdSuffix.get().getPath());
461462
}
462463
}
464+
Path baseDir = receiverFileDirWithIdSuffix.get().toPath().toAbsolutePath().normalize();
465+
Path targetPath = baseDir.resolve(fileName).toAbsolutePath().normalize();
463466

464-
writingFile = new File(receiverFileDirWithIdSuffix.get(), fileName);
467+
if (!targetPath.startsWith(baseDir)) {
468+
LOGGER.error(
469+
"Receiver id = {}: Path traversal attempt detected! Filename: {}",
470+
receiverId.get(),
471+
fileName);
472+
throw new IOException("Illegal fileName: " + fileName + " (Path traversal detected)");
473+
}
474+
475+
writingFile = targetPath.toFile();
465476
writingFileWriter = new RandomAccessFile(writingFile, "rw");
466477
LOGGER.info(
467478
"Receiver id = {}: Writing file {} was created. Ready to write file pieces.",
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 String getSenderHost() {
87+
return "127.0.0.1";
88+
}
89+
90+
@Override
91+
protected String getSenderPort() {
92+
return "6667";
93+
}
94+
95+
@Override
96+
protected String getClusterId() {
97+
return "test-cluster";
98+
}
99+
100+
@Override
101+
protected TSStatus login() {
102+
return new TSStatus(200);
103+
}
104+
105+
@Override
106+
protected TSStatus loadFileV1(
107+
final PipeTransferFileSealReqV1 req, final String fileAbsolutePath) {
108+
return new TSStatus(200);
109+
}
110+
111+
@Override
112+
protected TSStatus loadFileV2(
113+
final PipeTransferFileSealReqV2 req, final List<String> fileAbsolutePaths)
114+
throws IllegalPathException {
115+
return new TSStatus(200);
116+
}
117+
118+
@Override
119+
protected void closeSession() {
120+
// noop for unit test
121+
}
122+
123+
@Override
124+
public TPipeTransferResp receive(TPipeTransferReq req) {
125+
return null;
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)