This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/dev/1.3 by this push:
new d2abcffc53c Fix fail-open authorization for pipe config plans (#18488)
(#18511)
d2abcffc53c is described below
commit d2abcffc53cb2049f254f033837f3d86128b3e2b
Author: Caideyipi <[email protected]>
AuthorDate: Mon Aug 24 14:53:26 2026 +0800
Fix fail-open authorization for pipe config plans (#18488) (#18511)
* Fix fail-open authorization for pipe config plans
* Fix pipe device deletion authorization
* Harden pipe plugin path handling
* Fix pipe plugin metadata test import
* Deduplicate pipe plugin path validation
(cherry picked from commit 20ce15e04a56f37ad7721c95f023a3adf3f467a0)
---
.../receiver/protocol/IoTDBConfigNodeReceiver.java | 9 +-
.../pipe/agent/plugin/meta/PipePluginMeta.java | 13 ++-
.../service/PipePluginExecutableManager.java | 66 +++++++++---
.../org/apache/iotdb/commons/utils/FileUtils.java | 16 +++
.../service/PipePluginExecutableManagerTest.java | 112 +++++++++++++++++++++
.../pipe/plugin/meta/PipePluginMetaTest.java | 17 ++++
6 files changed, 211 insertions(+), 22 deletions(-)
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/receiver/protocol/IoTDBConfigNodeReceiver.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/receiver/protocol/IoTDBConfigNodeReceiver.java
index 10999474e9e..5d18b78d8fe 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/receiver/protocol/IoTDBConfigNodeReceiver.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/receiver/protocol/IoTDBConfigNodeReceiver.java
@@ -308,6 +308,7 @@ public class IoTDBConfigNodeReceiver extends
IoTDBFileReceiver {
username, Collections.emptyList(),
PrivilegeType.EXTEND_TEMPLATE.ordinal())
.getStatus();
case CreateSchemaTemplate:
+ case DropSchemaTemplate:
case CommitSetSchemaTemplate:
case PipeUnsetTemplate:
return
CommonDescriptor.getInstance().getConfig().getAdminName().equals(username)
@@ -411,7 +412,7 @@ public class IoTDBConfigNodeReceiver extends
IoTDBFileReceiver {
username, Collections.emptyList(),
PrivilegeType.MANAGE_ROLE.ordinal())
.getStatus();
default:
- return StatusUtils.OK;
+ return RpcUtils.getStatus(TSStatusCode.NO_PERMISSION);
}
}
@@ -517,10 +518,14 @@ public class IoTDBConfigNodeReceiver extends
IoTDBFileReceiver {
case CreateUser:
case CreateRole:
case CreateUserWithRawPassword:
- default:
+ case DropSchemaTemplate:
+ // Only explicitly supported config-region pipe plans may be written
to consensus. New plan
+ // types must be added to an explicit case after their authorization
is implemented.
return configManager
.getConsensusManager()
.write(shouldMarkAsPipeRequest.get() ? new PipeEnrichedPlan(plan)
: plan);
+ default:
+ return RpcUtils.getStatus(TSStatusCode.NO_PERMISSION);
}
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/meta/PipePluginMeta.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/meta/PipePluginMeta.java
index 0fb2314a1cf..3b90e39f4c2 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/meta/PipePluginMeta.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/meta/PipePluginMeta.java
@@ -19,6 +19,8 @@
package org.apache.iotdb.commons.pipe.agent.plugin.meta;
+import org.apache.iotdb.commons.utils.FileUtils;
+
import org.apache.tsfile.utils.PublicBAOS;
import org.apache.tsfile.utils.ReadWriteIOUtils;
@@ -26,6 +28,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import java.util.Locale;
import java.util.Objects;
public class PipePluginMeta {
@@ -41,21 +44,23 @@ public class PipePluginMeta {
public PipePluginMeta(
String pluginName, String className, boolean isBuiltin, String jarName,
String jarMD5) {
- this.pluginName = Objects.requireNonNull(pluginName).toUpperCase();
+ this.pluginName =
+
FileUtils.validatePathSegment(Objects.requireNonNull(pluginName)).toUpperCase(Locale.ROOT);
this.className = Objects.requireNonNull(className);
this.isBuiltin = isBuiltin;
if (isBuiltin) {
- this.jarName = jarName;
+ this.jarName = jarName == null ? null :
FileUtils.validatePathSegment(jarName);
this.jarMD5 = jarMD5;
} else {
- this.jarName = Objects.requireNonNull(jarName);
+ this.jarName =
FileUtils.validatePathSegment(Objects.requireNonNull(jarName));
this.jarMD5 = Objects.requireNonNull(jarMD5);
}
}
public PipePluginMeta(String pluginName, String className) {
- this.pluginName = Objects.requireNonNull(pluginName).toUpperCase();
+ this.pluginName =
+
FileUtils.validatePathSegment(Objects.requireNonNull(pluginName)).toUpperCase(Locale.ROOT);
this.className = Objects.requireNonNull(className);
this.isBuiltin = true;
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManager.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManager.java
index f113e33da07..3ecfc57ad6b 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManager.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManager.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.commons.pipe.agent.plugin.service;
import org.apache.iotdb.commons.executable.ExecutableManager;
import org.apache.iotdb.commons.file.SystemFileFactory;
import org.apache.iotdb.commons.pipe.agent.plugin.meta.PipePluginMeta;
+import org.apache.iotdb.commons.utils.FileUtils;
import org.apache.iotdb.pipe.api.exception.PipeException;
import org.apache.commons.codec.digest.DigestUtils;
@@ -34,6 +35,7 @@ import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Locale;
public class PipePluginExecutableManager extends ExecutableManager {
@@ -44,7 +46,7 @@ public class PipePluginExecutableManager extends
ExecutableManager {
}
public boolean isLocalJarMatched(PipePluginMeta pipePluginMeta) throws
PipeException {
- final String pluginName = pipePluginMeta.getPluginName();
+ final String pluginName =
FileUtils.validatePathSegment(pipePluginMeta.getPluginName());
final String md5FilePath = pluginName + ".txt";
if (hasFileUnderTemporaryRoot(md5FilePath)) {
@@ -60,7 +62,7 @@ public class PipePluginExecutableManager extends
ExecutableManager {
final String md5 =
DigestUtils.md5Hex(
Files.newInputStream(
- Paths.get(getPluginInstallPathV2(pluginName,
pipePluginMeta.getJarName()))));
+ getPluginInstallPathV2Path(pluginName,
pipePluginMeta.getJarName())));
// Save the md5 in a txt under trigger temporary lib
saveTextAsFileUnderTemporaryRoot(md5, md5FilePath);
return md5.equals(pipePluginMeta.getJarMD5());
@@ -95,32 +97,33 @@ public class PipePluginExecutableManager extends
ExecutableManager {
}
public boolean hasPluginFileUnderInstallDir(String pluginName, String
fileName) {
- return Files.exists(Paths.get(getPluginInstallPathV2(pluginName,
fileName)));
+ return Files.exists(getPluginInstallPathV2Path(pluginName, fileName));
}
public String getPluginsDirPath(String pluginName) {
- return this.libRoot + File.separator + INSTALL_DIR + File.separator +
pluginName.toUpperCase();
+ return getPluginDirectoryPath(pluginName).toString();
}
public void removePluginFileUnderLibRoot(String pluginName, String fileName)
throws IOException {
- String pluginPath = getPluginInstallPathV2(pluginName, fileName);
- Path path = Paths.get(pluginPath);
+ final Path path = getPluginInstallPathV2Path(pluginName, fileName);
Files.deleteIfExists(path);
Files.deleteIfExists(path.getParent());
}
public String getPluginInstallPathV2(String pluginName, String fileName) {
- return this.libRoot
- + File.separator
- + INSTALL_DIR
- + File.separator
- + pluginName.toUpperCase()
- + File.separator
- + fileName;
+ return getPluginInstallPathV2Path(pluginName, fileName).toString();
}
public String getPluginInstallPathV1(String fileName) {
- return this.libRoot + File.separator + INSTALL_DIR + File.separator +
fileName;
+ return resolvePathUnderDirectory(getInstallDirectoryPath(),
fileName).toString();
+ }
+
+ public void linkExistedPlugin(
+ final String oldPluginName, final String newPluginName, final String
fileName)
+ throws IOException {
+ FileUtils.createHardLink(
+ getPluginInstallPathV2Path(oldPluginName, fileName).toFile(),
+ getPluginInstallPathV2Path(newPluginName, fileName).toFile());
}
/**
@@ -131,7 +134,38 @@ public class PipePluginExecutableManager extends
ExecutableManager {
*/
public void savePluginToInstallDir(ByteBuffer byteBuffer, String pluginName,
String fileName)
throws IOException {
- String destination = getPluginInstallPathV2(pluginName, fileName);
- saveToDir(byteBuffer, destination);
+ saveToDir(byteBuffer, getPluginInstallPathV2Path(pluginName,
fileName).toString());
+ }
+
+ private Path getPluginInstallPathV2Path(final String pluginName, final
String fileName) {
+ return resolvePathUnderDirectory(getPluginDirectoryPath(pluginName),
fileName);
+ }
+
+ private Path getPluginDirectoryPath(final String pluginName) {
+ final String validatedPluginName =
FileUtils.validatePathSegment(pluginName);
+ return resolvePathUnderDirectory(
+ getInstallDirectoryPath(),
validatedPluginName.toUpperCase(Locale.ROOT));
+ }
+
+ private Path getInstallDirectoryPath() {
+ return Paths.get(libRoot, INSTALL_DIR).toAbsolutePath().normalize();
+ }
+
+ /**
+ * Resolves a single untrusted path segment below {@code baseDirectory}.
+ *
+ * <p>The segment validation rejects separators and dot segments, while the
normalized containment
+ * check remains as a defense in depth for absolute paths and future callers.
+ */
+ private Path resolvePathUnderDirectory(final Path baseDirectory, final
String pathSegment) {
+ FileUtils.validatePathSegment(pathSegment);
+
+ final Path normalizedBaseDirectory =
baseDirectory.toAbsolutePath().normalize();
+ final Path normalizedTargetPath =
+
normalizedBaseDirectory.resolve(pathSegment).toAbsolutePath().normalize();
+ if (!normalizedTargetPath.startsWith(normalizedBaseDirectory)) {
+ throw new IllegalArgumentException("Path traversal detected: " +
pathSegment);
+ }
+ return normalizedTargetPath;
}
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/FileUtils.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/FileUtils.java
index 01232f9237a..af558fae93b 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/FileUtils.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/FileUtils.java
@@ -39,6 +39,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
@@ -585,4 +586,19 @@ public class FileUtils {
}
return null;
}
+
+ /**
+ * Validates a path segment before it is used to construct a path.
+ *
+ * <p>In addition to the application-level checks above, constructing a
{@link Path} validates
+ * platform-specific path syntax (for example, NUL characters on Unix).
+ */
+ public static String validatePathSegment(final String pathSegment) {
+ final String pathError = getIllegalError4Directory(pathSegment);
+ if (pathError != null) {
+ throw new IllegalArgumentException(pathError);
+ }
+ Paths.get(pathSegment);
+ return pathSegment;
+ }
}
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManagerTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManagerTest.java
new file mode 100644
index 00000000000..03f4cb25cb9
--- /dev/null
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/agent/plugin/service/PipePluginExecutableManagerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.agent.plugin.service;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+public class PipePluginExecutableManagerTest {
+
+ @Test
+ public void testPluginPathsAreConfinedToInstallDirectory() throws Exception {
+ final Path root =
Files.createTempDirectory("pipe-plugin-executable-manager-test");
+ final Path temporaryLibRoot = root.resolve("temporary");
+ final Path libRoot = root.resolve("lib");
+ final PipePluginExecutableManager manager =
+ new PipePluginExecutableManager(temporaryLibRoot.toString(),
libRoot.toString());
+ final String traversalFileName = ".." + File.separator + ".." +
File.separator + "outside.jar";
+ final Path outsideFile = libRoot.resolve("outside.jar");
+ final byte[] pluginContent = "plugin".getBytes(StandardCharsets.UTF_8);
+
+ try {
+ Assert.assertEquals(
+ libRoot
+ .toAbsolutePath()
+ .normalize()
+ .resolve("install")
+ .resolve("TEST-PLUGIN")
+ .resolve("test.jar"),
+ Paths.get(manager.getPluginInstallPathV2("test-plugin",
"test.jar")));
+ manager.savePluginToInstallDir(ByteBuffer.wrap(pluginContent),
"test-plugin", "test.jar");
+ Assert.assertArrayEquals(
+ pluginContent,
+
Files.readAllBytes(Paths.get(manager.getPluginInstallPathV2("test-plugin",
"test.jar"))));
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> manager.getPluginsDirPath(".." + File.separator + "outside"));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> manager.getPluginsDirPath(".." + otherFileSeparator() +
"outside"));
+ Assert.assertThrows(
+ IllegalArgumentException.class, () ->
manager.getPluginInstallPathV1(traversalFileName));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ manager.savePluginToInstallDir(
+ ByteBuffer.wrap(new byte[] {1}), "plugin",
traversalFileName));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ manager.savePluginToInstallDir(
+ ByteBuffer.wrap(new byte[] {1}),
+ ".." + otherFileSeparator() + "plugin",
+ "test.jar"));
+ Assert.assertFalse(Files.exists(outsideFile));
+
+ Files.createDirectories(outsideFile.getParent());
+ Files.write(outsideFile, "preserve".getBytes(StandardCharsets.UTF_8));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> manager.removePluginFileUnderLibRoot("plugin",
traversalFileName));
+ Assert.assertArrayEquals(
+ "preserve".getBytes(StandardCharsets.UTF_8),
Files.readAllBytes(outsideFile));
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> manager.linkExistedPlugin("source", "target",
traversalFileName));
+ } finally {
+ deleteRecursively(root);
+ }
+ }
+
+ private static String otherFileSeparator() {
+ return File.separatorChar == '/' ? "\\" : "/";
+ }
+
+ private static void deleteRecursively(final Path path) throws IOException {
+ try (final Stream<Path> stream = Files.walk(path)) {
+ for (final Path subPath :
+ (Iterable<Path>) stream.sorted(Comparator.reverseOrder())::iterator)
{
+ Files.deleteIfExists(subPath);
+ }
+ }
+ }
+}
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/plugin/meta/PipePluginMetaTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/plugin/meta/PipePluginMetaTest.java
index b5b854adc4d..4aa8c5739a9 100644
---
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/plugin/meta/PipePluginMetaTest.java
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/plugin/meta/PipePluginMetaTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.commons.pipe.plugin.meta;
import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
import
org.apache.iotdb.commons.pipe.agent.plugin.meta.ConfigNodePipePluginMetaKeeper;
import
org.apache.iotdb.commons.pipe.agent.plugin.meta.DataNodePipePluginMetaKeeper;
+import org.apache.iotdb.commons.pipe.agent.plugin.meta.PipePluginMeta;
import org.junit.Assert;
import org.junit.Test;
@@ -58,4 +59,20 @@ public class PipePluginMetaTest {
BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginClass(),
keeper.getBuiltinPluginClass(BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginName()));
}
+
+ @Test
+ public void testRejectPathTraversalInPluginMetadata() {
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> new PipePluginMeta("../plugin", "test.Plugin", false,
"test.jar", "md5"));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> new PipePluginMeta("plugin", "test.Plugin", false,
"../test.jar", "md5"));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> new PipePluginMeta("plugin", "test.Plugin", false,
"..\\test.jar", "md5"));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> new PipePluginMeta("plugin\0", "test.Plugin", false, "test.jar",
"md5"));
+ }
}