mirror of
https://github.com/dataease/dataease.git
synced 2026-05-14 21:12:33 +08:00
fix: 修复上传svg图片存在XSS攻击问题 (#17999)
This commit is contained in:
@@ -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);
|
||||
|
||||
// 检查根元素是否是<svg>
|
||||
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<String> 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<String> 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<String> 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",
|
||||
"<script", "</script", "&#", "\\u"
|
||||
};
|
||||
|
||||
for (String pattern : jsPatterns) {
|
||||
if (lowerValue.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查base64编码的潜在脚本
|
||||
if (lowerValue.contains("base64")) {
|
||||
// 简单的base64解码检查,这里可以根据需要实现更复杂的检测
|
||||
return lowerValue.matches(".*base64\\s*,\\s*[a-z0-9+/=]{20,}.*");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文本节点是否包含脚本
|
||||
*/
|
||||
private static boolean containsScriptInText(Document doc) {
|
||||
NodeList allNodes = doc.getElementsByTagName("*");
|
||||
for (int i = 0; i < allNodes.getLength(); i++) {
|
||||
Node node = allNodes.item(i);
|
||||
|
||||
// 检查元素的文本内容
|
||||
String textContent = node.getTextContent();
|
||||
if (textContent != null && !textContent.trim().isEmpty()) {
|
||||
String lowerText = textContent.toLowerCase();
|
||||
|
||||
// 检查文本中是否包含HTML/XML标签
|
||||
if (lowerText.contains("<script") ||
|
||||
lowerText.contains("</script>") ||
|
||||
lowerText.contains("<?") ||
|
||||
lowerText.contains("<!")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否包含JavaScript代码
|
||||
if (containsJavaScript(lowerText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查CDATA节点
|
||||
NodeList childNodes = node.getChildNodes();
|
||||
for (int j = 0; j < childNodes.getLength(); j++) {
|
||||
Node child = childNodes.item(j);
|
||||
if (child.getNodeType() == Node.CDATA_SECTION_NODE) {
|
||||
String cdataContent = child.getTextContent().toLowerCase();
|
||||
if (containsJavaScript(cdataContent) ||
|
||||
cdataContent.contains("<script") ||
|
||||
cdataContent.contains("</script")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public static FileType getFileType(InputStream is) throws IOException {
|
||||
byte[] src = new byte[28];
|
||||
is.read(src, 0, 28);
|
||||
|
||||
Reference in New Issue
Block a user