CVE-2022-23544 (published 2022-12-28T00:15Z)

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.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
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.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/metersphere/metersphere
commit_details: repo_url: https://github.com/metersphere/metersphere
author: chenjianxing
author_date: 2022-12-08 16:49:42+08:00
msg: fix(测试跟踪): 缺陷平台请求转发添加白名单
repository: repo_url: https://github.com/metersphere/metersphere
repo_name: metersphere/metersphere
description: MeterSphere 是新一代的开源持续测试工具，让软件测试工作更简单、更高效，不再成为持续交付的瓶颈。
repo_language: Java
owner: metersphere
file_changes: filename: SessionFilter.java
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,7 @@ public class SessionFilter implements WebFilter {
     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;

programming_language: Java
filename: IssuesPlatform.java
change_type: ModificationType.MODIFY
diff: @@ -106,10 +106,10 @@ public interface IssuesPlatform {
 
     /**
      * Get请求的代理
-     * @param url
+     * @param path
      * @return
      */
-    ResponseEntity proxyForGet(String url, Class responseEntityClazz);
+    ResponseEntity proxyForGet(String path, Class responseEntityClazz);
 
     /**
      * 同步MS缺陷附件到第三方平台

programming_language: Java
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@
         <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>

programming_language: HTML
filename: IssueProxyResourceController.java
change_type: ModificationType.MODIFY
diff: @@ -15,9 +15,10 @@ 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);
     }
 }

programming_language: Java
filename: IssuesService.java
change_type: ModificationType.MODIFY
diff: @@ -59,7 +59,6 @@ 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;
@@ -697,8 +696,6 @@ public class IssuesService {
             item.setCaseCount(caseIdSet.size());
         });
         buildCustomField(issues);
-        //处理MD图片链接内容
-        handleJiraIssueMdUrl(request.getWorkspaceId(), request.getProjectId(), issues);
         return issues;
     }
 
@@ -789,35 +786,6 @@ public class IssuesService {
 
     }
 
-    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);

programming_language: Java
filename: PlatformPluginService.java
change_type: ModificationType.MODIFY
diff: @@ -88,6 +88,7 @@ public class PlatformPluginService {
         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) {

programming_language: Java
filename: ZentaoClient.java
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,6 @@ 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;
@@ -13,6 +12,8 @@ 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 {
@@ -244,4 +245,19 @@ public abstract class ZentaoClient extends BaseClient {
                 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);
+    }
 }

programming_language: Java
filename: AbstractIssuePlatform.java
change_type: ModificationType.MODIFY
diff: @@ -6,7 +6,6 @@ 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.*;
@@ -32,6 +31,7 @@ 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;
@@ -65,6 +65,8 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
     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;
     }
@@ -114,6 +116,10 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
         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();
@@ -333,7 +339,7 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform {
         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));

programming_language: Java
filename: ZentaoPlatform.java
change_type: ModificationType.MODIFY
diff: @@ -492,7 +492,7 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
         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) {
@@ -565,15 +565,17 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
                     }
                 } 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 + ")";
@@ -682,4 +684,9 @@ public class ZentaoPlatform extends AbstractIssuePlatform {
         }
         return platformStatusDTOS;
     }
+
+    @Override
+    public ResponseEntity proxyForGet(String path, Class responseEntityClazz) {
+        return zentaoClient.proxyForGet(path, responseEntityClazz);
+    }
 }

programming_language: Java
filename: IssueProxyResourceService.java
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,10 @@
 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;
@@ -25,19 +25,20 @@ 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);
     }
 }

programming_language: Java