fix: 修复上传svg图片存在XSS攻击问题 (#17999)

This commit is contained in:
王嘉豪
2026-03-02 16:41:09 +08:00
committed by GitHub
parent a0e295c5d8
commit e96eb9355e

View File

@@ -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);