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:29 UTC

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

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