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

wolfboys pushed a commit to branch dev-2.1.8
in repository https://gitbox.apache.org/repos/asf/streampark.git


The following commit(s) were added to refs/heads/dev-2.1.8 by this push:
     new f574febd4 fix: validate application config file reads (#4351)
f574febd4 is described below

commit f574febd4a0b6b3c11b4020474ee77d13990cd60
Author: benjobs <[email protected]>
AuthorDate: Thu May 28 07:50:13 2026 +0800

    fix: validate application config file reads (#4351)
    
    * Update version to 2.1.8
    
    * fix(console): validate pagination sort fields
    
    Validate user-provided pagination sort fields before building MyBatis order 
clauses.
---
 .../core/controller/ApplicationController.java     |   7 +-
 .../console/core/service/ApplicationService.java   |   4 +-
 .../core/service/impl/ApplicationServiceImpl.java  |  59 ++++++-
 .../service/impl/ApplicationServiceImplTest.java   | 169 +++++++++++++++++++++
 .../src/api/flink/app/app.ts                       |   4 +-
 .../src/views/flink/app/Add.vue                    |   2 +
 .../src/views/flink/app/components/AppConf.tsx     |   4 +
 .../src/views/flink/app/hooks/useCreateSchema.ts   |   2 +
 8 files changed, 240 insertions(+), 11 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 411473a3f..5a84d7300 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
@@ -177,7 +177,7 @@ public class ApplicationController {
 
   @PostMapping("name")
   @PermissionScope(app = "#app.id", team = "#app.teamId")
-  public RestResponse yarnName(Application app) {
+  public RestResponse yarnName(Application app) throws IOException {
     String yarnName = applicationService.getYarnName(app);
     return RestResponse.success(yarnName);
   }
@@ -190,8 +190,9 @@ public class ApplicationController {
   }
 
   @PostMapping("readConf")
-  public RestResponse readConf(String config) throws IOException {
-    String content = applicationService.readConf(config);
+  @PermissionScope(team = "#app.teamId")
+  public RestResponse readConf(Application app) throws IOException {
+    String content = applicationService.readConf(app);
     return RestResponse.success(content);
   }
 
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/ApplicationService.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/ApplicationService.java
index cfc887f0d..fcfe8e6b6 100644
--- 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/ApplicationService.java
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/service/ApplicationService.java
@@ -54,7 +54,7 @@ public interface ApplicationService extends 
IService<Application> {
 
   void restart(Application application) throws Exception;
 
-  String getYarnName(Application app);
+  String getYarnName(Application app) throws IOException;
 
   AppExistsState checkExists(Application app);
 
@@ -66,7 +66,7 @@ public interface ApplicationService extends 
IService<Application> {
 
   void clean(Application app);
 
-  String readConf(String config) throws IOException;
+  String readConf(Application application) throws IOException;
 
   Application getApp(Application application);
 
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 3dc870365..fea28a077 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
@@ -699,10 +699,11 @@ public class ApplicationServiceImpl extends 
ServiceImpl<ApplicationMapper, Appli
   }
 
   @Override
-  public String getYarnName(Application appParam) {
+  public String getYarnName(Application appParam) throws IOException {
+    File file = getReadableConfFile(appParam);
     String[] args = new String[2];
     args[0] = "--name";
-    args[1] = appParam.getConfig();
+    args[1] = file.getAbsolutePath();
     return ParameterCli.read(args);
   }
 
@@ -1213,12 +1214,62 @@ public class ApplicationServiceImpl extends 
ServiceImpl<ApplicationMapper, Appli
   }
 
   @Override
-  public String readConf(String config) throws IOException {
-    File file = new File(config);
+  public String readConf(Application application) throws IOException {
+    File file = getReadableConfFile(application);
     String conf = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
     return Base64.getEncoder().encodeToString(conf.getBytes());
   }
 
+  @VisibleForTesting
+  File getReadableConfFile(Application application) throws IOException {
+    ApiAlertException.throwIfNull(application, "Invalid application.");
+    ApiAlertException.throwIfNull(application.getProjectId(), "Invalid 
project.");
+    ApiAlertException.throwIfNull(application.getTeamId(), "Invalid team.");
+    
ApiAlertException.throwIfTrue(StringUtils.isBlank(application.getModule()), 
"Invalid module.");
+    ApiAlertException.throwIfTrue(
+        StringUtils.containsAny(application.getModule(), '/', '\\'), "Invalid 
module.");
+    
ApiAlertException.throwIfTrue(StringUtils.isBlank(application.getConfig()), 
"Invalid config.");
+
+    Project project = projectService.getById(application.getProjectId());
+    ApiAlertException.throwIfNull(project, "Invalid project.");
+    ApiAlertException.throwIfFalse(
+        application.getTeamId().equals(project.getTeamId()), "Invalid 
project.");
+
+    File projectDistHome = project.getDistHome().getCanonicalFile();
+    ApiAlertException.throwIfFalse(projectDistHome.isDirectory(), "Invalid 
project.");
+
+    File moduleArchive = new File(projectDistHome, 
application.getModule()).getCanonicalFile();
+    File moduleHome =
+        new File(StringUtils.removeEnd(moduleArchive.getAbsolutePath(), 
".tar.gz"))
+            .getCanonicalFile();
+    ApiAlertException.throwIfFalse(
+        isDirectChildPath(projectDistHome, moduleArchive), "Invalid module.");
+    ApiAlertException.throwIfFalse(
+        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(confHome.isDirectory(), "Invalid config.");
+
+    File configFile = new File(application.getConfig()).getCanonicalFile();
+
+    ApiAlertException.throwIfFalse(configFile.isFile(), "Invalid config.");
+    ApiAlertException.throwIfFalse(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);
diff --git 
a/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImplTest.java
 
b/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImplTest.java
new file mode 100644
index 000000000..b5c5d3610
--- /dev/null
+++ 
b/streampark-console/streampark-console-service/src/test/java/org/apache/streampark/console/core/service/impl/ApplicationServiceImplTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.Application;
+import org.apache.streampark.console.core.entity.Project;
+import org.apache.streampark.console.core.service.ProjectService;
+
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ApplicationServiceImplTest {
+
+  @Mock private ProjectService projectService;
+
+  private final ApplicationServiceImpl applicationService = new 
ApplicationServiceImpl();
+
+  @TempDir private Path tempDir;
+
+  @BeforeEach
+  void setUp() {
+    ReflectionTestUtils.setField(applicationService, "projectService", 
projectService);
+  }
+
+  @Test
+  void getReadableConfFileShouldAllowConfigUnderProjectModuleConf() throws 
Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path configFile =
+        Files.createDirectories(distHome.resolve("app").resolve("conf"))
+            .resolve("application.yaml");
+    Files.write(configFile, "key: value".getBytes());
+
+    when(projectService.getById(1L)).thenReturn(project(1L, 10L, distHome));
+
+    Application application = application(1L, 10L, "app.tar.gz", configFile);
+
+    assertThat(applicationService.getReadableConfFile(application))
+        .isEqualTo(configFile.toFile().getCanonicalFile());
+  }
+
+  @Test
+  void getReadableConfFileShouldRejectConfigOutsideProjectModuleConf() throws 
Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Files.createDirectories(distHome.resolve("app").resolve("conf"));
+    Path secretFile = Files.write(tempDir.resolve("secret.txt"), 
"secret".getBytes());
+
+    when(projectService.getById(1L)).thenReturn(project(1L, 10L, distHome));
+
+    Application application = application(1L, 10L, "app.tar.gz", secretFile);
+
+    assertThatThrownBy(() -> 
applicationService.getReadableConfFile(application))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid config.");
+  }
+
+  @Test
+  void getReadableConfFileShouldRejectModuleTraversal() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path outsideModule = 
Files.createDirectories(tempDir.resolve("outside").resolve("conf"));
+    Path configFile =
+        Files.write(outsideModule.resolve("application.yaml"), "key: 
value".getBytes());
+
+    Application application = application(1L, 10L, "../outside.tar.gz", 
configFile);
+
+    assertThatThrownBy(() -> 
applicationService.getReadableConfFile(application))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void getReadableConfFileShouldRejectModulePathAlias() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path configFile =
+        Files.createDirectories(distHome.resolve("app").resolve("conf"))
+            .resolve("application.yaml");
+    Files.write(configFile, "key: value".getBytes());
+
+    Application application = application(1L, 10L, "app/conf/..", configFile);
+
+    assertThatThrownBy(() -> 
applicationService.getReadableConfFile(application))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid module.");
+  }
+
+  @Test
+  void getReadableConfFileShouldRejectProjectFromOtherTeam() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path configFile =
+        Files.createDirectories(distHome.resolve("app").resolve("conf"))
+            .resolve("application.yaml");
+    Files.write(configFile, "key: value".getBytes());
+
+    when(projectService.getById(1L)).thenReturn(project(1L, 20L, distHome));
+
+    Application application = application(1L, 10L, "app.tar.gz", configFile);
+
+    assertThatThrownBy(() -> 
applicationService.getReadableConfFile(application))
+        .isInstanceOf(ApiAlertException.class)
+        .hasMessage("Invalid project.");
+  }
+
+  @Test
+  void getYarnNameShouldReadOnlyValidatedConfig() throws Exception {
+    Path distHome = Files.createDirectory(tempDir.resolve("dist"));
+    Path configFile =
+        Files.createDirectories(distHome.resolve("app").resolve("conf"))
+            .resolve("application.properties");
+    Files.write(configFile, 
"flink.property.pipeline.name=test-app".getBytes());
+
+    when(projectService.getById(1L)).thenReturn(project(1L, 10L, distHome));
+
+    Application application = application(1L, 10L, "app.tar.gz", configFile);
+
+    
assertThat(applicationService.getYarnName(application)).isEqualTo("test-app");
+  }
+
+  private Application application(Long projectId, Long teamId, String module, 
Path configFile) {
+    Application application = new Application();
+    application.setProjectId(projectId);
+    application.setTeamId(teamId);
+    application.setModule(module);
+    application.setConfig(configFile.toString());
+    return application;
+  }
+
+  private Project project(Long id, Long teamId, Path distHome) {
+    return new Project() {
+      {
+        setId(id);
+        setTeamId(teamId);
+      }
+
+      @Override
+      public File getDistHome() {
+        return distHome.toFile();
+      }
+    };
+  }
+}
diff --git 
a/streampark-console/streampark-console-webapp/src/api/flink/app/app.ts 
b/streampark-console/streampark-console-webapp/src/api/flink/app/app.ts
index 0b9259e3c..3cdee9912 100644
--- a/streampark-console/streampark-console-webapp/src/api/flink/app/app.ts
+++ b/streampark-console/streampark-console-webapp/src/api/flink/app/app.ts
@@ -53,7 +53,7 @@ enum APP_API {
  * read configuration file
  * @returns Promise<any>
  */
-export function fetchAppConf(params?: { config: any }) {
+export function fetchAppConf(params?: { projectId?: any; module?: any; config: 
any }) {
   return defHttp.post<any>({
     url: APP_API.READ_CONF,
     params,
@@ -226,6 +226,6 @@ export function fetchCancel(data: CancelParam): 
Promise<boolean> {
   return defHttp.post({ url: APP_API.CANCEL, data });
 }
 
-export function fetchName(data: { config: string }) {
+export function fetchName(data: { projectId?: any; module?: any; config: 
string }) {
   return defHttp.post({ url: APP_API.NAME, data });
 }
diff --git 
a/streampark-console/streampark-console-webapp/src/views/flink/app/Add.vue 
b/streampark-console/streampark-console-webapp/src/views/flink/app/Add.vue
index 974ca7dde..0fa58193d 100644
--- a/streampark-console/streampark-console-webapp/src/views/flink/app/Add.vue
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/Add.vue
@@ -188,6 +188,8 @@
           params['format'] = getAppConfType(configVal);
           if (values.configOverride == null) {
             params['config'] = await fetchAppConf({
+              projectId: params.projectId,
+              module: params.module,
               config: configVal,
             });
           } else {
diff --git 
a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppConf.tsx
 
b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppConf.tsx
index 9a036cd7c..3c970fda3 100644
--- 
a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppConf.tsx
+++ 
b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppConf.tsx
@@ -62,9 +62,13 @@ export default defineComponent({
 
     async function handleChangeNewConfig(confFile: string) {
       const appName = await fetchName({
+        projectId: unref(model).project,
+        module: unref(model).module,
         config: confFile,
       });
       const appConf = await fetchAppConf({
+        projectId: unref(model).project,
+        module: unref(model).module,
         config: confFile,
       });
       model.value.config = confFile;
diff --git 
a/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateSchema.ts
 
b/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateSchema.ts
index 847d9371e..71657702a 100644
--- 
a/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateSchema.ts
+++ 
b/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateSchema.ts
@@ -263,6 +263,8 @@ export const useCreateSchema = (dependencyRef: Ref) => {
             fieldNames: { children: 'children', label: 'title', key: 'value', 
value: 'value' },
             onChange: (value: string) => {
               fetchName({
+                projectId: formModel.project,
+                module: formModel.module,
                 config: value,
               }).then((resp) => {
                 formModel.jobName = resp;

Reply via email to