CVE-2022-23544: Server-Side Request Forgery (SSRF) (CWE-918)
value: MeterSphere is a one-stop open source continuous testing platform, covering test management, interface testing, UI testing and performance testing. Versions prior to 2.5.0 are subject to a Server-Side Request Forgery that leads to Cross-Site Scripting. A Server-Side request forgery in `IssueProxyResourceService::getMdImageByUrl` allows an attacker to access internal resources, as well as executing JavaScript code in the context of Metersphere's origin by a victim of a reflected XSS. This vulnerability has been fixed in v2.5.0. There are no known workarounds.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 6.1
Published: 2022-12-28T00:15Z
Repository: https://github.com/metersphere/metersphere
Language: Java
Files changed: framework/gateway/src/main/java/io/metersphere/gateway/filter/SessionFilter.java, framework/sdk-parent/xpack-interface/src/main/java/io/metersphere/xpack/track/issue/IssuesPlatform.java, pom.xml, test-track/backend/src/main/java/io/metersphere/controller/IssueProxyResourceController.java, test-track/backend/src/main/java/io/metersphere/service/IssuesService.java, test-track/backend/src/main/java/io/metersphere/service/PlatformPluginService.java, test-track/backend/src/main/java/io/metersphere/service/issue/client/ZentaoClient.java, test-track/backend/src/main/java/io/metersphere/service/issue/platform/AbstractIssuePlatform.java, test-track/backend/src/main/java/io/metersphere/service/issue/platform/ZentaoPlatform.java, test-track/backend/src/main/java/io/metersphere/service/wapper/IssueProxyResourceService.java
Commit date: 2022-12-09 13:44:46 +0800
Commit message: fix(测试跟踪): 缺陷平台请求转发添加白名单

Diff: diff --git a/framework/gateway/src/main/java/io/metersphere/gateway/filter/SessionFilter.java b/framework/gateway/src/main/java/io/metersphere/gateway/filter/SessionFilter.java
index 844c3b7b9..f9389e4ff 100644
--- a/framework/gateway/src/main/java/io/metersphere/gateway/filter/SessionFilter.java
+++ b/framework/gateway/src/main/java/io/metersphere/gateway/filter/SessionFilter.java
@@ -19,11 +19,11 @@ public class SessionFilter implements WebFilter {
     // 所有模块的前缀
     private static final String[] PREFIX = new String[]{"/setting", "/project", "/api", "/performance", "/track", "/workstation", "/ui", "/report"};
     private static final String[] TO_SUB_SERVICE = new String[]{"/license", "/system", "/resource", "/sso/callback/logout", "/sso/callback/cas/logout"};
     private static final String PERFORMANCE_DOWNLOAD_PREFIX = "/jmeter/";
     private static final String API_DOWNLOAD_PREFIX = "/api/jmeter/";
-    private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/url";
+    private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/path";
 
     @Resource
     private DiscoveryClient discoveryClient;
     @Value("${spring.application.name}")
     private String serviceName;
diff --git a/framework/sdk-parent/xpack-interface/src/main/java/io/metersphere/xpack/track/issue/IssuesPlatform.java b/framework/sdk-parent/xpack-interface/src/main/java/io/metersphere/xpack/track/issue/IssuesPlatform.java
index a4babfa75..8ec415e82 100644
--- a/framework/sdk-parent/xpack-interface/src/main/java/io/metersphere/xpack/track/issue/IssuesPlatform.java
+++ b/framework/sdk-parent/xpack-interface/src/main/java/io/metersphere/xpack/track/issue/IssuesPlatform.java
@@ -104,14 +104,14 @@ public interface IssuesPlatform {
      */
     void updateDemandHyperLink(EditTestCaseRequest request, Project project, String type);
 
     /**
      * Get请求的代理
-     * @param url
+     * @param path
      * @return
      */
-    ResponseEntity proxyForGet(String url, Class responseEntityClazz);
+    ResponseEntity proxyForGet(String path, Class responseEntityClazz);
 
     /**
      * 同步MS缺陷附件到第三方平台
      * @param issuesRequest 平台参数
      * @param file 附件
diff --git a/pom.xml b/pom.xml
index 681e50d05..965179b7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,11 +21,11 @@
         <revision>main</revision>
         <java.version>11</java.version>
         <spring-cloud.version>2021.0.5</spring-cloud.version>
         <spring-security.version>5.7.5</spring-security.version>
         <dubbo.version>2.7.18</dubbo.version>
-        <platform-plugin-sdk.version>1.0.0</platform-plugin-sdk.version>
+        <platform-plugin-sdk.version>1.1.0</platform-plugin-sdk.version>
         <flyway.version>7.15.0</flyway.version>
         <shiro.version>1.10.1</shiro.version>
         <mssql-jdbc.version>7.4.1.jre8</mssql-jdbc.version>
         <postgresql.version>42.3.8</postgresql.version>
         <java-websocket.version>1.5.3</java-websocket.version>
diff --git a/test-track/backend/src/main/java/io/metersphere/controller/IssueProxyResourceController.java b/test-track/backend/src/main/java/io/metersphere/controller/IssueProxyResourceController.java
index b02e5eb1f..a35f8485f 100644
--- a/test-track/backend/src/main/java/io/metersphere/controller/IssueProxyResourceController.java
+++ b/test-track/backend/src/main/java/io/metersphere/controller/IssueProxyResourceController.java
@@ -13,11 +13,12 @@ import javax.annotation.Resource;
 @RequestMapping(value = "/resource")
 public class IssueProxyResourceController {
     @Resource
     IssueProxyResourceService issueProxyResourceService;
 
-    @GetMapping(value = "/md/get/url")
-    public ResponseEntity<byte[]> getFileByUrl(@RequestParam ("url") String url, @RequestParam (value = "platform", required = false) String platform,
-                                               @RequestParam (value = "workspace_id", required = false) String workspaceId) {
-        return issueProxyResourceService.getMdImageByUrl(url, platform, workspaceId);
+    @GetMapping(value = "/md/get/path")
+    public ResponseEntity<byte[]> getFileByPath(@RequestParam ("path") String path,
+                                                @RequestParam (value = "platform") String platform,
+                                                @RequestParam (value = "workspaceId") String workspaceId) {
+        return issueProxyResourceService.getMdImageByPath(path, platform, workspaceId);
     }
 }
diff --git a/test-track/backend/src/main/java/io/metersphere/service/IssuesService.java b/test-track/backend/src/main/java/io/metersphere/service/IssuesService.java
index 89fe4974c..8c9bd9f0f 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/IssuesService.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/IssuesService.java
@@ -57,11 +57,10 @@ import io.metersphere.xpack.track.dto.PlatformStatusDTO;
 import io.metersphere.xpack.track.dto.PlatformUser;
 import io.metersphere.xpack.track.dto.*;
 import io.metersphere.xpack.track.dto.request.IssuesRequest;
 import io.metersphere.xpack.track.dto.request.IssuesUpdateRequest;
 import io.metersphere.xpack.track.issue.IssuesPlatform;
-import jodd.util.CollectionUtil;
 import io.metersphere.xpack.track.service.XpackIssueService;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -695,12 +694,10 @@ public class IssuesService {
             }
             item.setCaseIds(new ArrayList<>(caseIdSet));
             item.setCaseCount(caseIdSet.size());
         });
         buildCustomField(issues);
-        //处理MD图片链接内容
-        handleJiraIssueMdUrl(request.getWorkspaceId(), request.getProjectId(), issues);
         return issues;
     }
 
     private void buildCustomField(List<IssuesDao> data) {
         if (CollectionUtils.isEmpty(data)) {
@@ -787,39 +784,10 @@ public class IssuesService {
             MSException.throwException(e.getMessage());
         }
 
     }
 
-    private void handleJiraIssueMdUrl(String workPlaceId, String projectId, List<IssuesDao> issues) {
-        issues.forEach(issue -> {
-            if (StringUtils.isNotEmpty(issue.getDescription()) && issue.getDescription().contains("platform=Jira&")) {
-                issue.setDescription(replaceJiraMdUrlParam(issue.getDescription(), workPlaceId, projectId));
-            }
-            if (StringUtils.isNotEmpty(issue.getCustomFields()) && issue.getCustomFields().contains("platform=Jira&")) {
-                issue.setCustomFields(replaceJiraMdUrlParam(issue.getCustomFields(), workPlaceId, projectId));
-            }
-            if (CollectionUtils.isNotEmpty(issue.getFields())) {
-                issue.getFields().forEach(field -> {
-                    if (StringUtils.isNotEmpty(field.getTextValue()) && field.getTextValue().contains("platform=Jira&")) {
-                        field.setTextValue(replaceJiraMdUrlParam(field.getTextValue(), workPlaceId, projectId));
-                    }
-                    if (StringUtils.isNotEmpty(field.getValue()) && field.getValue().contains("platform=Jira&")) {
-                        field.setValue(replaceJiraMdUrlParam(field.getValue(), workPlaceId, projectId));
-                    }
-                });
-            }
-        });
-    }
-
-    private String replaceJiraMdUrlParam(String url, String workspaceId, String projectId) {
-        if (url.contains("&workspace_id=")) {
-            return url;
-        }
-        return url.replaceAll("platform=Jira&",
-                "platform=Jira&workspace_id=" + workspaceId + "&");
-    }
-
     private Map<String, List<IssueCommentDTO>> getCommentMap(List<IssuesDao> issues) {
         List<String> issueIds = issues.stream().map(IssuesDao::getId).collect(Collectors.toList());
         List<IssueCommentDTO> comments = extIssueCommentMapper.getCommentsByIssueIds(issueIds);
         Map<String, List<IssueCommentDTO>> commentMap = comments.stream().collect(Collectors.groupingBy(IssueCommentDTO::getIssueId));
         return commentMap;
diff --git a/test-track/backend/src/main/java/io/metersphere/service/PlatformPluginService.java b/test-track/backend/src/main/java/io/metersphere/service/PlatformPluginService.java
index a2340bcdf..00af2a67e 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/PlatformPluginService.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/PlatformPluginService.java
@@ -86,10 +86,11 @@ public class PlatformPluginService {
         integrationRequest.setPlatform(platformKey);
         integrationRequest.setWorkspaceId(StringUtils.isBlank(workspaceId) ? SessionUtils.getCurrentWorkspaceId() : workspaceId);
         ServiceIntegration serviceIntegration = baseIntegrationService.get(integrationRequest);
 
         PlatformRequest pluginRequest = new PlatformRequest();
+        pluginRequest.setWorkspaceId(workspaceId);
         pluginRequest.setIntegrationConfig(serviceIntegration.getConfiguration());
         Platform platform = getPluginManager().getPlatformByKey(platformKey, pluginRequest);
         if (platform == null) {
             MSException.throwException(Translator.get("platform_plugin_not_exit") + PLUGIN_DOWNLOAD_URL);
         }
diff --git a/test-track/backend/src/main/java/io/metersphere/service/issue/client/ZentaoClient.java b/test-track/backend/src/main/java/io/metersphere/service/issue/client/ZentaoClient.java
index 230d7cd4e..8a00b39e7 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/issue/client/ZentaoClient.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/issue/client/ZentaoClient.java
@@ -2,19 +2,20 @@ package io.metersphere.service.issue.client;
 
 import io.metersphere.commons.exception.MSException;
 import io.metersphere.commons.utils.JSON;
 import io.metersphere.commons.utils.LogUtil;
 import io.metersphere.commons.utils.UnicodeConvertUtils;
-import io.metersphere.i18n.Translator;
 import io.metersphere.service.issue.domain.zentao.*;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.http.*;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 
 import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.Map;
 
 public abstract class ZentaoClient extends BaseClient {
 
     protected String ENDPOINT;
@@ -242,6 +243,21 @@ public abstract class ZentaoClient extends BaseClient {
         String sessionId = login();
         ResponseEntity<byte[]> response = restTemplate.exchange(requestUrl.getFileDownload(), HttpMethod.GET,
                 null, byte[].class, fileId, sessionId);
         return response.getBody();
     }
+
+    public ResponseEntity proxyForGet(String path, Class responseEntityClazz) {
+        im.metersphere.plugin.utils.LogUtil.info("zentao proxyForGet: " + path);
+        String url = this.ENDPOINT + path;
+        try {
+            if (!StringUtils.containsAny(new URI(url).getPath(), "/index.php", "/file-read-")) {
+                // 只允许访问图片
+                MSException.throwException("illegal path");
+            }
+        } catch (URISyntaxException e) {
+            LogUtil.error(e);
+            MSException.throwException("illegal path");
+        }
+        return restTemplate.exchange(url, HttpMethod.GET, null, responseEntityClazz);
+    }
 }
diff --git a/test-track/backend/src/main/java/io/metersphere/service/issue/platform/AbstractIssuePlatform.java b/test-track/backend/src/main/java/io/metersphere/service/issue/platform/AbstractIssuePlatform.java
index 340cc8825..95ceed3f2 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/issue/platform/AbstractIssuePlatform.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/issue/platform/AbstractIssuePlatform.java
@@ -4,11 +4,10 @@ import io.metersphere.base.domain.*;
 import io.metersphere.base.mapper.AttachmentModuleRelationMapper;
 import io.metersphere.base.mapper.IssuesMapper;
 import io.metersphere.base.mapper.TestCaseIssuesMapper;
 import io.metersphere.base.mapper.ext.ExtIssuesMapper;
 import io.metersphere.commons.constants.CustomFieldType;
-import io.metersphere.commons.constants.IssueRefType;
 import io.metersphere.commons.constants.IssuesStatus;
 import io.metersphere.commons.exception.MSException;
 import io.metersphere.commons.utils.*;
 import io.metersphere.dto.CustomFieldItemDTO;
 import io.metersphere.dto.UserDTO;
@@ -30,10 +29,11 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.MultiValueMap;
 
 import java.io.File;
 import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -63,10 +63,12 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
     protected FileService fileService;
     protected AttachmentService attachmentService;
     protected AttachmentModuleRelationMapper attachmentModuleRelationMapper;
     protected BaseProjectService baseProjectService;
 
+    public static final String PROXY_PATH = "/resource/md/get/path?platform=%s&workspaceId=%s&path=%s";
+
     public String getKey() {
         return key;
     }
 
     public AbstractIssuePlatform(IssuesRequest issuesRequest) {
@@ -112,10 +114,14 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
 
         ServiceIntegration integration = baseIntegrationService.get(request);
         return integration.getConfiguration();
     }
 
+    protected String getProxyPath(String path) {
+        return String.format(PROXY_PATH, this.key, this.workspaceId, URLEncoder.encode(path, StandardCharsets.UTF_8));
+    }
+
     protected HttpHeaders auth(String apiUser, String password) {
         String authKey = EncryptUtils.base64Encoding(apiUser + ":" + password);
         HttpHeaders headers = new HttpHeaders();
         headers.add("Authorization", "Basic " + authKey);
         return headers;
@@ -331,11 +337,11 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
         }
         Matcher matcher = pattern.matcher(input);
         while (matcher.find()) {
             try {
                 String path = matcher.group(2);
-                if (!path.contains("/resource/md/get/url")) {
+                if (!path.contains("/resource/md/get/url") && !path.contains("/resource/md/get/path")) {
                     if (path.contains("/resource/md/get/")) { // 兼容旧数据
                         String name = path.substring(path.indexOf("/resource/md/get/") + 17);
                         files.add(new File(FileUtils.MD_IMAGE_DIR + "/" + name));
                     } else if (path.contains("/resource/md/get")) { // 新数据走这里
                         String name = path.substring(path.indexOf("/resource/md/get") + 26);
diff --git a/test-track/backend/src/main/java/io/metersphere/service/issue/platform/ZentaoPlatform.java b/test-track/backend/src/main/java/io/metersphere/service/issue/platform/ZentaoPlatform.java
index 857b2d67e..c93fc8f7e 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/issue/platform/ZentaoPlatform.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/issue/platform/ZentaoPlatform.java
@@ -490,11 +490,11 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
         String zentaoSteps = msDescription.replaceAll(imgUrlRegex, zentaoClient.requestUrl.getReplaceImgUrl());
         Matcher matcher = zentaoClient.requestUrl.getImgPattern().matcher(zentaoSteps);
         while (matcher.find()) {
             // get file name
             String originSubUrl = matcher.group(1);
-            if (originSubUrl.contains("/url?url=")) {
+            if (originSubUrl.contains("/url?url=") || originSubUrl.contains("/path?")) {
                 String path = URLDecoder.decode(originSubUrl, StandardCharsets.UTF_8);
                 String fileName;
                 if (path.indexOf("fileID") > 0) {
                     fileName = path.substring(path.indexOf("fileID") + 7);
                 } else {
@@ -563,19 +563,21 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
                     } else {
                         return result;
                     }
                 } else {
                     name = name.replaceAll("&amp;", "&");
-                    try {
-                        URI uri = new URI(zentaoClient.getBaseUrl());
-                        path = uri.getScheme() + "://" + uri.getHost() + path.replaceAll("&amp;", "&");
-                    } catch (URISyntaxException e) {
-                        path = zentaoClient.getBaseUrl() + path.replaceAll("&amp;", "&");
-                        LogUtil.error(e);
+                    path = path.replaceAll("&amp;", "&");
+                }
+                StringBuilder stringBuilder = new StringBuilder();
+                for (String item : path.split("&")) {
+                    // 去掉多余的参数
+                    if (!StringUtils.containsAny(item, "platform", "workspaceId")) {
+                        stringBuilder.append(item);
+                        stringBuilder.append("&");
                     }
                 }
-                path = "/resource/md/get/url?url=" + URLEncoder.encode(path, StandardCharsets.UTF_8);
+                path = getProxyPath(stringBuilder.toString());
             }
             // 图片与描述信息之间需换行，否则无法预览图片
             result = "\n\n![" + name + "](" + path + ")";
         }
 
@@ -680,6 +682,11 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
 
             platformStatusDTOS.add(platformStatusDTO);
         }
         return platformStatusDTOS;
     }
+
+    @Override
+    public ResponseEntity proxyForGet(String path, Class responseEntityClazz) {
+        return zentaoClient.proxyForGet(path, responseEntityClazz);
+    }
 }
diff --git a/test-track/backend/src/main/java/io/metersphere/service/wapper/IssueProxyResourceService.java b/test-track/backend/src/main/java/io/metersphere/service/wapper/IssueProxyResourceService.java
index f50964102..3e74ee78e 100644
--- a/test-track/backend/src/main/java/io/metersphere/service/wapper/IssueProxyResourceService.java
+++ b/test-track/backend/src/main/java/io/metersphere/service/wapper/IssueProxyResourceService.java
@@ -1,12 +1,12 @@
 package io.metersphere.service.wapper;
 
-import io.metersphere.commons.exception.MSException;
-import io.metersphere.i18n.Translator;
+import io.metersphere.commons.constants.IssuesManagePlatform;
 import io.metersphere.service.PlatformPluginService;
+import io.metersphere.service.issue.platform.IssueFactory;
+import io.metersphere.xpack.track.dto.request.IssuesRequest;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.http.HttpMethod;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.client.RestTemplate;
 
@@ -23,21 +23,22 @@ public class IssueProxyResourceService {
 
     /**
      * http 代理
      * 如果当前访问地址是 https，直接访问 http 的图片资源
      * 由于浏览器的安全机制，http 会被转成 https
-     * @param url
+     * @param path
      * @param platform
      * @return
      */
-    public ResponseEntity<byte[]> getMdImageByUrl(String url, String platform, String workspaceId) {
-        if (url.contains("md/get/url")) {
-            MSException.throwException(Translator.get("invalid_parameter"));
-        }
-        if (StringUtils.isNotBlank(platform)) {
-            return platformPluginService.getPlatform(platform, workspaceId)
-                    .proxyForGet(url, byte[].class);
+    public ResponseEntity<byte[]> getMdImageByPath(String path, String platform, String workspaceId) {
+        if (StringUtils.equals(IssuesManagePlatform.Zentao.name(), platform)) {
+            IssuesRequest issuesRequest = new IssuesRequest();
+            issuesRequest.setWorkspaceId(workspaceId);
+            return IssueFactory.createPlatform(platform, issuesRequest)
+                    .proxyForGet(path, byte[].class);
 
+        } else {
+            return platformPluginService.getPlatform(platform, workspaceId)
+                    .proxyForGet(path, byte[].class);
         }
-        return restTemplate.exchange(url, HttpMethod.GET, null, byte[].class);
     }
 }

Vulnerable code:     private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/url";
     * @param url
    ResponseEntity proxyForGet(String url, Class responseEntityClazz);
        <platform-plugin-sdk.version>1.0.0</platform-plugin-sdk.version>
    @GetMapping(value = "/md/get/url")
    public ResponseEntity<byte[]> getFileByUrl(@RequestParam ("url") String url, @RequestParam (value = "platform", required = false) String platform,
                                               @RequestParam (value = "workspace_id", required = false) String workspaceId) {
        return issueProxyResourceService.getMdImageByUrl(url, platform, workspaceId);
import jodd.util.CollectionUtil;
        //处理MD图片链接内容
        handleJiraIssueMdUrl(request.getWorkspaceId(), request.getProjectId(), issues);
    private void handleJiraIssueMdUrl(String workPlaceId, String projectId, List<IssuesDao> issues) {
        issues.forEach(issue -> {
            if (StringUtils.isNotEmpty(issue.getDescription()) && issue.getDescription().contains("platform=Jira&")) {
                issue.setDescription(replaceJiraMdUrlParam(issue.getDescription(), workPlaceId, projectId));
            }
            if (StringUtils.isNotEmpty(issue.getCustomFields()) && issue.getCustomFields().contains("platform=Jira&")) {
                issue.setCustomFields(replaceJiraMdUrlParam(issue.getCustomFields(), workPlaceId, projectId));
            }
            if (CollectionUtils.isNotEmpty(issue.getFields())) {
                issue.getFields().forEach(field -> {
                    if (StringUtils.isNotEmpty(field.getTextValue()) && field.getTextValue().contains("platform=Jira&")) {
                        field.setTextValue(replaceJiraMdUrlParam(field.getTextValue(), workPlaceId, projectId));
                    }
                    if (StringUtils.isNotEmpty(field.getValue()) && field.getValue().contains("platform=Jira&")) {
                        field.setValue(replaceJiraMdUrlParam(field.getValue(), workPlaceId, projectId));
                    }
                });
            }
        });
    }

    private String replaceJiraMdUrlParam(String url, String workspaceId, String projectId) {
        if (url.contains("&workspace_id=")) {
            return url;
        }
        return url.replaceAll("platform=Jira&",
                "platform=Jira&workspace_id=" + workspaceId + "&");
    }

import io.metersphere.i18n.Translator;
import io.metersphere.commons.constants.IssueRefType;
                if (!path.contains("/resource/md/get/url")) {
            if (originSubUrl.contains("/url?url=")) {
                    try {
                        URI uri = new URI(zentaoClient.getBaseUrl());
                        path = uri.getScheme() + "://" + uri.getHost() + path.replaceAll("&amp;", "&");
                    } catch (URISyntaxException e) {
                        path = zentaoClient.getBaseUrl() + path.replaceAll("&amp;", "&");
                        LogUtil.error(e);
                path = "/resource/md/get/url?url=" + URLEncoder.encode(path, StandardCharsets.UTF_8);
import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import org.springframework.http.HttpMethod;
     * @param url
    public ResponseEntity<byte[]> getMdImageByUrl(String url, String platform, String workspaceId) {
        if (url.contains("md/get/url")) {
            MSException.throwException(Translator.get("invalid_parameter"));
        }
        if (StringUtils.isNotBlank(platform)) {
            return platformPluginService.getPlatform(platform, workspaceId)
                    .proxyForGet(url, byte[].class);
        return restTemplate.exchange(url, HttpMethod.GET, null, byte[].class);
Fixed code:     private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/path";
     * @param path
    ResponseEntity proxyForGet(String path, Class responseEntityClazz);
        <platform-plugin-sdk.version>1.1.0</platform-plugin-sdk.version>
    @GetMapping(value = "/md/get/path")
    public ResponseEntity<byte[]> getFileByPath(@RequestParam ("path") String path,
                                                @RequestParam (value = "platform") String platform,
                                                @RequestParam (value = "workspaceId") String workspaceId) {
        return issueProxyResourceService.getMdImageByPath(path, platform, workspaceId);
        pluginRequest.setWorkspaceId(workspaceId);
import java.net.URI;
import java.net.URISyntaxException;

    public ResponseEntity proxyForGet(String path, Class responseEntityClazz) {
        im.metersphere.plugin.utils.LogUtil.info("zentao proxyForGet: " + path);
        String url = this.ENDPOINT + path;
        try {
            if (!StringUtils.containsAny(new URI(url).getPath(), "/index.php", "/file-read-")) {
                // 只允许访问图片
                MSException.throwException("illegal path");
            }
        } catch (URISyntaxException e) {
            LogUtil.error(e);
            MSException.throwException("illegal path");
        }
        return restTemplate.exchange(url, HttpMethod.GET, null, responseEntityClazz);
    }
import java.net.URLEncoder;
    public static final String PROXY_PATH = "/resource/md/get/path?platform=%s&workspaceId=%s&path=%s";

    protected String getProxyPath(String path) {
        return String.format(PROXY_PATH, this.key, this.workspaceId, URLEncoder.encode(path, StandardCharsets.UTF_8));
    }

                if (!path.contains("/resource/md/get/url") && !path.contains("/resource/md/get/path")) {
            if (originSubUrl.contains("/url?url=") || originSubUrl.contains("/path?")) {
                    path = path.replaceAll("&amp;", "&");
                }
                StringBuilder stringBuilder = new StringBuilder();
                for (String item : path.split("&")) {
                    // 去掉多余的参数
                    if (!StringUtils.containsAny(item, "platform", "workspaceId")) {
                        stringBuilder.append(item);
                        stringBuilder.append("&");
                path = getProxyPath(stringBuilder.toString());

    @Override
    public ResponseEntity proxyForGet(String path, Class responseEntityClazz) {
        return zentaoClient.proxyForGet(path, responseEntityClazz);
    }
import io.metersphere.commons.constants.IssuesManagePlatform;
import io.metersphere.service.issue.platform.IssueFactory;
import io.metersphere.xpack.track.dto.request.IssuesRequest;
     * @param path
    public ResponseEntity<byte[]> getMdImageByPath(String path, String platform, String workspaceId) {
        if (StringUtils.equals(IssuesManagePlatform.Zentao.name(), platform)) {
            IssuesRequest issuesRequest = new IssuesRequest();
            issuesRequest.setWorkspaceId(workspaceId);
            return IssueFactory.createPlatform(platform, issuesRequest)
                    .proxyForGet(path, byte[].class);
        } else {
            return platformPluginService.getPlatform(platform, workspaceId)
                    .proxyForGet(path, byte[].class);