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

Reply via email to