diff --git a/core/core-backend/src/main/java/io/dataease/visualization/server/StaticResourceServer.java b/core/core-backend/src/main/java/io/dataease/visualization/server/StaticResourceServer.java index 3deeb12a45..984d369a1e 100644 --- a/core/core-backend/src/main/java/io/dataease/visualization/server/StaticResourceServer.java +++ b/core/core-backend/src/main/java/io/dataease/visualization/server/StaticResourceServer.java @@ -16,6 +16,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; import org.xml.sax.SAXException; import javax.imageio.ImageIO; @@ -28,10 +33,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; @RestController @RequestMapping("/staticResource") @@ -158,6 +160,12 @@ public class StaticResourceServer implements StaticResourceApi { return false; } + // MIME类型预检查 + if (!isValidSvgMimeType(file)) { + DEException.throwException("无效的SVG文件MIME类型"); + return false; + } + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); try (InputStream inputStream = file.getInputStream()) { @@ -167,25 +175,204 @@ public class StaticResourceServer implements StaticResourceApi { dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setNamespaceAware(true); + + // 启用安全解析设置 + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(inputStream); // 检查根元素是否是 - if ("svg".equals(doc.getDocumentElement().getNodeName())) { - return true; - } else { + if (!"svg".equals(doc.getDocumentElement().getNodeName())) { + DEException.throwException("根元素必须是svg"); return false; } + + // 安全检查:如果发现任何危险内容,直接返回false + if (containsDangerousContent(doc)) { + DEException.throwException("SVG包含不允许的脚本或事件处理器"); + return false; + } + + return true; + } catch (ParserConfigurationException | SAXException | IOException e) { // 如果出现任何解析错误,说明该文件不是合法的SVG - if(e.getMessage() != null && e.getMessage().indexOf("DOCTYPE")>-1){ + if(e.getMessage() != null && e.getMessage().contains("DOCTYPE")){ DEException.throwException("svg 内容禁止使用 DOCTYPE"); - }else { - DEException.throwException(e); + } else { + DEException.throwException("SVG解析失败: " + e.getMessage()); } } return false; } + + /** + * MIME类型检查 + */ + private static boolean isValidSvgMimeType(MultipartFile file) { + String contentType = file.getContentType(); + if (contentType == null) { + return false; + } + + // 允许的SVG MIME类型 + Set allowedMimeTypes = new HashSet<>(Arrays.asList( + "image/svg+xml", + "image/svg-xml", + "application/svg+xml" + )); + + return allowedMimeTypes.contains(contentType.toLowerCase()); + } + + /** + * 检查SVG是否包含危险内容 + * @return true 包含危险内容,false 安全 + */ + private static boolean containsDangerousContent(Document doc) { + // 危险的事件处理器属性 + Set dangerousAttributes = new HashSet<>(Arrays.asList( + "onload", "onerror", "onclick", "ondblclick", "onmousedown", "onmouseup", + "onmouseover", "onmousemove", "onmouseout", "onfocus", "onblur", + "onkeydown", "onkeypress", "onkeyup", "onsubmit", "onreset", + "onchange", "onselect", "onabort", "onunload", "onresize", + "onscroll", "oninput", "onactivate", "onbeforeactivate", "onbeforedeactivate", + "ondeactivate", "onbegin", "onend", "onrepeat", "onloadstart", + "onprogress", "onloadend", "oncanplay", "oncanplaythrough", "onwaiting", + "onseeking", "onseeked", "ontimeupdate", "onplaying", "onpause", + "onratechange", "ondurationchange", "onvolumechange" + )); + + // 危险标签 + Set dangerousTags = new HashSet<>(Arrays.asList( + "script", "style", "object", "embed", "applet", "iframe", + "frame", "frameset", "link", "meta", "base", "form" + )); + + // 遍历所有元素 + NodeList elements = doc.getElementsByTagName("*"); + for (int i = 0; i < elements.getLength(); i++) { + Element element = (Element) elements.item(i); + String tagName = element.getTagName().toLowerCase(); + + // 检查危险标签 + if (dangerousTags.contains(tagName)) { + return true; // 发现危险标签,返回true表示包含危险内容 + } + + // 检查属性 + NamedNodeMap attributes = element.getAttributes(); + for (int j = 0; j < attributes.getLength(); j++) { + Attr attr = (Attr) attributes.item(j); + String attrName = attr.getName().toLowerCase(); + String attrValue = attr.getValue().toLowerCase(); + + // 检查事件处理器属性 + if (dangerousAttributes.contains(attrName)) { + return true; + } + + // 检查属性名是否以"on"开头(事件处理器) + if (attrName.startsWith("on") && attrName.length() > 2) { + return true; + } + + // 检查属性值是否包含JavaScript相关代码 + if (containsJavaScript(attrValue)) { + return true; + } + } + } + + // 检查文本节点 + if (containsScriptInText(doc)) { + return true; + } + + return false; + } + + /** + * 检查字符串是否包含JavaScript代码 + */ + private static boolean containsJavaScript(String value) { + if (value == null || value.isEmpty()) { + return false; + } + + String lowerValue = value.toLowerCase(); + + // JavaScript相关模式 + String[] jsPatterns = { + "javascript:", "vbscript:", "data:", "expression(", + "eval(", "alert(", "confirm(", "prompt(", + "document.", "window.", "location.", "cookie.", + "function(", "new function", "settimeout(", "setinterval(", + "innerhtml", "outerhtml", "insertadjacenthtml", + "") || + lowerText.contains("