You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampark.apache.org by be...@apache.org on 2026/07/31 23:32:27 UTC

(streampark) branch fix/validate-module-name created (now 6b8d60f90)

This is an automated email from the ASF dual-hosted git repository.

wolfboys pushed a change to branch fix/validate-module-name
in repository https://gitbox.apache.org/repos/asf/streampark.git


      at 6b8d60f90 [Improve] validate module name before file resolution

This branch includes the following new commits:

     new 72ef4919a [Refactor] extract path containment helpers into PathUtils
     new 6b8d60f90 [Improve] validate module name before file resolution

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(streampark) 01/02: [Refactor] extract path containment helpers into PathUtils

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

wolfboys pushed a commit to branch fix/validate-module-name
in repository https://gitbox.apache.org/repos/asf/streampark.git

commit 72ef4919a0dc37c6d5af7fe04a4927ef040f9457
Author: benjobs <be...@apache.org>
AuthorDate: Sat Aug 1 07:28:45 2026 +0800

    [Refactor] extract path containment helpers into PathUtils
    
    Move the isDescendantPath/isDirectChildPath checks from
    ApplicationServiceImpl (private) into a shared base/util PathUtils class
    so the same canonical-path containment logic can be reused elsewhere.
    Pure refactor, no behavior change.
---
 .../streampark/console/base/util/PathUtils.java    | 64 ++++++++++++++++++++++
 .../core/service/impl/ApplicationServiceImpl.java  | 22 +++-----
 2 files changed, 71 insertions(+), 15 deletions(-)

diff --git a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/base/util/PathUtils.java b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/base/util/PathUtils.java
new file mode 100644
index 000000000..c7ddd1265
--- /dev/null
+++ b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/base/util/PathUtils.java
@@ -0,0 +1,64 @@
+/*
+ * 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.streampark.console.base.util;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Filesystem path containment utilities.
+ *
+ * <p>All checks resolve the canonical (absolute, normalized, symlink-resolved) paths before
+ * comparison, so the supplied name is always confined to the expected directory.
+ */
+public final class PathUtils {
+
+  private PathUtils() {}
+
+  /**
+   * Returns {@code true} only when the canonical path of {@code child} is strictly located
+   * somewhere <em>inside</em> {@code parent} (at any depth). A {@code child} that equals {@code
+   * parent} is NOT considered a descendant.
+   *
+   * @param parent the allowed root directory
+   * @param child the path to check
+   * @return {@code true} if {@code child} is strictly under {@code parent}
+   * @throws IOException if canonical path resolution fails
+   */
+  public static boolean isDescendantPath(File parent, File child) throws IOException {
+    String parentPath = parent.getCanonicalPath();
+    String childPath = child.getCanonicalPath();
+    return childPath.startsWith(parentPath.concat(File.separator));
+  }
+
+  /**
+   * Returns {@code true} only when the canonical parent directory of {@code child} is exactly
+   * {@code parent}, i.e. {@code child} is a <em>direct</em> (first-level) entry of {@code parent}.
+   * This is stricter than {@link #isDescendantPath(File, File)} and is used to confine a supplied
+   * name (e.g. a module archive) to a single level under the expected directory.
+   *
+   * @param parent the allowed root directory
+   * @param child the path to check
+   * @return {@code true} if {@code child} sits directly under {@code parent}
+   * @throws IOException if canonical path resolution fails
+   */
+  public static boolean isDirectChildPath(File parent, File child) throws IOException {
+    File childParent = child.getCanonicalFile().getParentFile();
+    return childParent != null && parent.getCanonicalFile().equals(childParent.getCanonicalFile());
+  }
+}
diff --git a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImpl.java b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImpl.java
index fea28a077..edb8208ee 100644
--- a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImpl.java
+++ b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImpl.java
@@ -42,6 +42,7 @@ import org.apache.streampark.console.base.exception.ApplicationException;
 import org.apache.streampark.console.base.mybatis.pager.MybatisPager;
 import org.apache.streampark.console.base.util.CommonUtils;
 import org.apache.streampark.console.base.util.ObjectUtils;
+import org.apache.streampark.console.base.util.PathUtils;
 import org.apache.streampark.console.base.util.WebUtils;
 import org.apache.streampark.console.core.bean.AppControl;
 import org.apache.streampark.console.core.bean.MavenDependency;
@@ -1243,33 +1244,24 @@ public class ApplicationServiceImpl extends ServiceImpl<ApplicationMapper, Appli
         new File(StringUtils.removeEnd(moduleArchive.getAbsolutePath(), ".tar.gz"))
             .getCanonicalFile();
     ApiAlertException.throwIfFalse(
-        isDirectChildPath(projectDistHome, moduleArchive), "Invalid module.");
+        PathUtils.isDirectChildPath(projectDistHome, moduleArchive), "Invalid module.");
     ApiAlertException.throwIfFalse(
-        isDirectChildPath(projectDistHome, moduleHome), "Invalid module.");
+        PathUtils.isDirectChildPath(projectDistHome, moduleHome), "Invalid module.");
     ApiAlertException.throwIfFalse(moduleHome.isDirectory(), "Invalid module.");
 
     File confHome = new File(moduleHome, "conf").getCanonicalFile();
-    ApiAlertException.throwIfFalse(isDescendantPath(moduleHome, confHome), "Invalid config.");
+    ApiAlertException.throwIfFalse(
+        PathUtils.isDescendantPath(moduleHome, confHome), "Invalid config.");
     ApiAlertException.throwIfFalse(confHome.isDirectory(), "Invalid config.");
 
     File configFile = new File(application.getConfig()).getCanonicalFile();
 
     ApiAlertException.throwIfFalse(configFile.isFile(), "Invalid config.");
-    ApiAlertException.throwIfFalse(isDescendantPath(confHome, configFile), "Invalid config.");
+    ApiAlertException.throwIfFalse(
+        PathUtils.isDescendantPath(confHome, configFile), "Invalid config.");
     return configFile;
   }
 
-  private boolean isDescendantPath(File parent, File child) throws IOException {
-    String parentPath = parent.getCanonicalPath();
-    String childPath = child.getCanonicalPath();
-    return childPath.startsWith(parentPath.concat(File.separator));
-  }
-
-  private boolean isDirectChildPath(File parent, File child) throws IOException {
-    File childParent = child.getCanonicalFile().getParentFile();
-    return childParent != null && parent.getCanonicalFile().equals(childParent.getCanonicalFile());
-  }
-
   @Override
   public Application getApp(Application appParam) {
     Application application = this.baseMapper.getApp(appParam);


(streampark) 02/02: [Improve] validate module name before file resolution

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

wolfboys pushed a commit to branch fix/validate-module-name
in repository https://gitbox.apache.org/repos/asf/streampark.git

commit 6b8d60f904c88ebc042bc1cb1051c84959d9dd34
Author: benjobs <be...@apache.org>
AuthorDate: Sat Aug 1 07:28:54 2026 +0800

    [Improve] validate module name before file resolution
    
    Validate the project module name in jars/listConf/getAppConfPath before
    resolving it against the project distribution home, so the resolved path
    is always a direct child of the expected directory. Also remove the
    unused checkjar endpoint that had no callers.
    
    Adds ProjectServiceImplTest covering module-name validation.
---
 .../core/controller/ApplicationController.java     |  13 --
 .../core/service/impl/ProjectServiceImpl.java      |  35 +++++-
 .../core/service/impl/ProjectServiceImplTest.java  | 136 +++++++++++++++++++++
 3 files changed, 167 insertions(+), 17 deletions(-)

diff --git a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/controller/ApplicationController.java b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/controller/ApplicationController.java
index 5a84d7300..9620ed329 100644
--- a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/controller/ApplicationController.java
+++ b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/controller/ApplicationController.java
@@ -17,7 +17,6 @@
 
 package org.apache.streampark.console.core.controller;
 
-import org.apache.streampark.common.util.Utils;
 import org.apache.streampark.common.util.YarnUtils;
 import org.apache.streampark.console.base.domain.RestRequest;
 import org.apache.streampark.console.base.domain.RestResponse;
@@ -43,7 +42,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
 import java.net.URI;
@@ -240,17 +238,6 @@ public class ApplicationController {
     return RestResponse.success(deleted);
   }
 
-  @PostMapping("checkjar")
-  public RestResponse checkjar(String jar) {
-    File file = new File(jar);
-    try {
-      Utils.checkJarFile(file.toURI().toURL());
-      return RestResponse.success(true);
-    } catch (IOException e) {
-      return RestResponse.success(file).message(e.getLocalizedMessage());
-    }
-  }
-
   @PostMapping("upload")
   @RequiresPermissions("app:create")
   public RestResponse upload(MultipartFile file) throws Exception {
diff --git a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ProjectServiceImpl.java b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ProjectServiceImpl.java
index ba72d4331..303966958 100644
--- a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ProjectServiceImpl.java
+++ b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/impl/ProjectServiceImpl.java
@@ -34,6 +34,7 @@ import org.apache.streampark.console.base.util.FileUtils;
 import org.apache.streampark.console.base.util.GZipUtils;
 import org.apache.streampark.console.base.util.GitUtils;
 import org.apache.streampark.console.base.util.ObjectUtils;
+import org.apache.streampark.console.base.util.PathUtils;
 import org.apache.streampark.console.core.entity.Application;
 import org.apache.streampark.console.core.entity.Project;
 import org.apache.streampark.console.core.enums.BuildState;
@@ -45,6 +46,7 @@ import org.apache.streampark.console.core.service.ProjectService;
 import org.apache.streampark.console.core.task.FlinkAppHttpWatcher;
 import org.apache.streampark.console.core.task.ProjectBuildTask;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.flink.configuration.MemorySize;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -270,9 +272,7 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project>
   @Override
   public List<String> jars(Project project) {
     List<String> list = new ArrayList<>(0);
-    ApiAlertException.throwIfNull(
-        project.getModule(), "Project module can't be null, please check.");
-    File apps = new File(project.getDistHome(), project.getModule());
+    File apps = resolveModuleDir(project);
     for (File file : Objects.requireNonNull(apps.listFiles())) {
       if (file.getName().endsWith(".jar")) {
         list.add(file.getName());
@@ -283,6 +283,8 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project>
 
   @Override
   public String getAppConfPath(Long id, String module) {
+    ApiAlertException.throwIfTrue(StringUtils.isBlank(module), "Invalid module.");
+    ApiAlertException.throwIfTrue(StringUtils.containsAny(module, '/', '\\'), "Invalid module.");
     Project project = getById(id);
     File appHome = project.getDistHome();
     File[] files = appHome.listFiles();
@@ -365,8 +367,10 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project>
 
   @Override
   public List<Map<String, Object>> listConf(Project project) {
+    // Validate the module name BEFORE entering the try-block, so an invalid name is rejected
+    // with ApiAlertException instead of being swallowed by the file-operation catch below.
+    File file = resolveModuleDir(project);
     try {
-      File file = new File(project.getDistHome(), project.getModule());
       File unzipFile = new File(file.getAbsolutePath().replaceAll(".tar.gz", ""));
       if (!unzipFile.exists()) {
         GZipUtils.decompress(file.getAbsolutePath(), file.getParentFile().getAbsolutePath());
@@ -385,6 +389,29 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project>
     return null;
   }
 
+  /**
+   * Resolves the module directory/file after validating that the module name is a single-level
+   * entry located directly under the project distribution home.
+   *
+   * @param project the project whose module directory to resolve
+   * @return the canonical module file
+   */
+  private File resolveModuleDir(Project project) {
+    ApiAlertException.throwIfTrue(
+        StringUtils.isBlank(project.getModule()), "Project module can't be null, please check.");
+    ApiAlertException.throwIfTrue(
+        StringUtils.containsAny(project.getModule(), '/', '\\'), "Invalid module.");
+    try {
+      File projectDistHome = project.getDistHome().getCanonicalFile();
+      File moduleDir = new File(projectDistHome, project.getModule()).getCanonicalFile();
+      ApiAlertException.throwIfFalse(
+          PathUtils.isDirectChildPath(projectDistHome, moduleDir), "Invalid module.");
+      return moduleDir;
+    } catch (IOException e) {
+      throw new ApiAlertException("Invalid module.", e);
+    }
+  }
+
   private void eachFile(File file, List<Map<String, Object>> list, Boolean isRoot) {
     if (file != null && file.exists() && file.listFiles() != null) {
       if (isRoot) {
diff --git a/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ProjectServiceImplTest.java b/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ProjectServiceImplTest.java
new file mode 100644
index 000000000..c7f9dfb24
--- /dev/null
+++ b/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ProjectServiceImplTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.streampark.console.core.service.impl;
+
+import org.apache.streampark.console.base.exception.ApiAlertException;
+import org.apache.streampark.console.core.entity.Project;
+import org.apache.streampark.console.core.mapper.ProjectMapper;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link ProjectServiceImpl} module-name validation on the {@code jars}, {@code
+ * listConf} and {@code getAppConfPath} methods.
+ */
+@ExtendWith(MockitoExtension.class)
+class ProjectServiceImplTest {
+
+  @Mock private ProjectMapper projectMapper;
+
+  private final ProjectServiceImpl projectService = new ProjectServiceImpl();
+
+  @TempDir private Path tempDir;
+
+  @BeforeEach
+  void setUp() {
+    // ServiceImpl#getById delegates to baseMapper.selectById(...).
+    ReflectionTestUtils.setField(projectService, "baseMapper", projectMapper);
+  }
+
+  @Test
+  void jarsShouldListJarFilesUnderProjectModule() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path moduleDir = Files.createDirectory(distHome.resolve("app"));
+    Files.createFile(moduleDir.resolve("foo.jar"));
+    Files.createFile(moduleDir.resolve("README.txt"));
+
+    List<String> jars = projectService.jars(project(1L, distHome, "app"));
+
+    // Only direct children ending with .jar are returned.
+    assertThat(jars).containsExactly("foo.jar");
+  }
+
+  @Test
+  void jarsShouldRejectRelativeModule() throws Exception {
+    Files.createDirectories(tempDir.resolve("outside"));
+
+    assertThatThrownBy(() -> projectService.jars(project(1L, tempDir, "../outside")))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void jarsShouldRejectModuleWithPathSeparator() throws Exception {
+    assertThatThrownBy(() -> projectService.jars(project(1L, tempDir, "a/b")))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void listConfShouldRejectRelativeModule() {
+    assertThatThrownBy(() -> projectService.listConf(project(1L, tempDir, "../../etc")))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void getAppConfPathShouldRejectModuleWithPathSeparator() {
+    assertThatThrownBy(() -> projectService.getAppConfPath(1L, "../etc"))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void getAppConfPathShouldRejectBlankModule() {
+    assertThatThrownBy(() -> projectService.getAppConfPath(1L, " "))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void getAppConfPathShouldResolveValidModule() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Files.createDirectories(distHome.resolve("app"));
+    when(projectMapper.selectById(1L)).thenReturn(project(1L, distHome, null));
+
+    String confPath = projectService.getAppConfPath(1L, "app");
+
+    assertThat(confPath).isEqualTo(distHome.resolve("app").toFile().getAbsolutePath());
+  }
+
+  private Project project(Long id, Path distHome, String module) {
+    return new Project() {
+      {
+        setId(id);
+        if (module != null) {
+          setModule(module);
+        }
+      }
+
+      @Override
+      public File getDistHome() {
+        return distHome.toFile();
+      }
+    };
+  }
+}