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;