问题
当我尝试上传下面的 SVG 时,它会消耗所有内存,最终主线程冻结并发生窗口捕捉。
重现步骤
运行下面的代码片段,您将看到它将捕捉结果面板,或者您可以尝试创建一个
svg
文件并上传它,然后也会发生同样的事情
我所知道的
这基本上是一种安全威胁,当用户上传此类恶意文件时通常称为
SVG Billion Laugh Attack
,最终会消耗浏览器的巨大内存。
解决方法
我想停止这样的文件上传,我认为如果我可以识别这么大的 SVG 上传,这是可能的,以某种方式可以跟踪它是否消耗巨大的内存,说任何特定的限制,如果违反了该限制,我可以简单地阻止用户正在上传。
提前致谢
<svg version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" xml:space="preserve">
<path id="a" d="M0,0"/>
<g id="b"><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/></g>
<g id="c"><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/></g>
<g id="d"><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/></g>
<g id="e"><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/></g>
<g id="f"><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/></g>
<g id="g"><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/></g>
<g id="h"><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/></g>
<g id="i"><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/></g>
<g id="j"><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/></g>
</svg>
可能不是最优雅的方式,但由于 svg 是基于 xml 的,
我们可以通过分析文件输入数据来查询潜在的恶意代码在上传之前:
[!entity]
new DOMParser().parseFromString(markup, "image/svg+xml")
解析文件输入数据以查询潜在的恶意或不需要的元素(例如<script>
标签)使用的
analyzeSVG(markup, allowed)
辅助函数检查元素出现情况,例如:
<use>
元素<use>
实例总数向下滚动到测试文件上传 - 检查控制台输出以获取详细反馈。
function validateSVG(markup, allowed = {}) {
// set defaults
if (!Object.keys(allowed).length) {
allowed = {
useElsNested: 100,
nonsensePaths: 0,
hasScripts: false,
hasEntity: false,
fileSizeKB: 500,
isSymbolSprite: false,
isSvgFont: false
}
}
let fileReport = analyzeSVG(markup, allowed);
let isValid = true;
let log = [];
if (!fileReport.totalEls) {
log.push('no elements')
isValid = false;
}
if (Object.keys(fileReport).length) {
if (fileReport.isBillionLaugh === true) {
log.push(`suspicious: might contain billion laugh attack`)
isValid = false;
}
for (let key in allowed) {
let val = allowed[key];
let valRep = fileReport[key];
if (typeof val === 'number' && valRep > val) {
log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `)
isValid = false;
}
if (valRep === true && val === false) {
log.push(`not allowed: "${key}" `)
isValid = false;
}
}
} else {
isValid = false;
}
if (!isValid) {
log = ['SVG not valid'].concat(log);
console.log(log.join('\n'));
if (Object.keys(fileReport).length) {
console.log(fileReport);
}
}
return isValid
}
function analyzeSVG(markup, allowed = {}) {
let doc, svg;
let fileReport = {};
let maxNested = allowed.useElsNested ? allowed.useElsNested : 3000;
/**
* analyze nestes use references
*/
const countUseRefs = (useEls, maxNested = 200) => {
let nestedCount = 0;
//stop loop if number of nested use references is exceeded
for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
let use = useEls[i];
let refId = use.getAttribute("xlink:href") ?
use.getAttribute("xlink:href") :
use.getAttribute("href");
refId = refId ? refId.replace("#", "") : "";
//normalize href attributes to facilitate JS selection
use.setAttribute("href", "#" + refId);
let refEl = svg.getElementById(refId);
let nestedUse = refEl.querySelectorAll("use");
let nestedUseLength = nestedUse.length;
nestedCount += nestedUseLength;
// query nested use references
for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
let nested = nestedUse[n];
let id1 = nested.getAttribute("href").replace("#", "");
let refEl1 = svg.getElementById(id1);
let nestedUse1 = refEl1.querySelectorAll("use");
nestedCount += nestedUse1.length;
}
}
fileReport.useElsNested = nestedCount;
return nestedCount;
};
/**
* check on raw text level
*/
let hasPrologue = /\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g.test(markup);
let hasEntity = /\<\!ENTITY/gi.test(markup);
// Contains xml entity definition: highly suspicious - stop parsing!
if (allowed.hasEntity === false && hasEntity) {
fileReport.hasEntity = true;
return fileReport;
}
/**
* sanitizing for parsing:
* remove xml prologue and comments
*/
markup = markup
.replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
.replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
/**
* Try to parse svg:
* invalid svg will return false via "catch"
*/
try {
doc = new DOMParser().parseFromString(markup, "image/svg+xml");
svg = doc.querySelector("svg");
// paths containing only a M command
let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]');
// create analyzing object
fileReport = {
totalEls: svg.querySelectorAll("*").length,
geometryEls: svg.querySelectorAll(
"path, rect, circle, ellipse, polygon, polyline, line"
).length,
useEls: svg.querySelectorAll("use").length,
useElsNested: 0,
nonsensePaths: nonsensePaths.length,
isSuspicious: false,
isBillionLaugh: false,
hasScripts: svg.querySelectorAll("script").length ? true : false,
hasPrologue: hasPrologue,
hasEntity: hasEntity,
fileSizeKB: +(new Blob([markup]).size / 1024).toFixed(3),
hasXmlns: svg.getAttribute('xmlns') ? (svg.getAttribute('xmlns') === 'http://www.w3.org/2000/svg' ? true : false) : false,
isSymbolSprite: svg.querySelectorAll('symbol').length && svg.querySelectorAll('use').length === 0 ? true : false,
isSvgFont: svg.querySelectorAll('glyph').length ? true : false
};
let totalEls = fileReport.totalEls;
let totalUseEls = fileReport.useEls;
let usePercentage = (100 / totalEls) * totalUseEls;
// if percentage of use elements is higher than 75% - suspicious
if (usePercentage > 75) {
fileReport.isSuspicious = true;
// check nested use references
let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
if (nestedCount >= maxNested) {
fileReport.isBillionLaugh = true;
}
}
return fileReport;
}
// svg file has malformed markup
catch {
console.log("svg could not be parsed");
return false;
}
}
textarea {
width: 100%;
min-height: 10em;
}
<h3>Malicious svg 1 (billion laugh nested use)</h3>
<textarea id="filecheck1">
<svg version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" xml:space="preserve">
<path id="a" d="M0,0"/>
<g id="b"><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/><use xlink:href="#a"/></g>
<g id="c"><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/><use xlink:href="#b"/></g>
<g id="d"><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/><use xlink:href="#c"/></g>
<g id="e"><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/><use xlink:href="#d"/></g>
<g id="f"><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/><use xlink:href="#e"/></g>
<g id="g"><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/><use xlink:href="#f"/></g>
<g id="h"><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/><use xlink:href="#g"/></g>
<g id="i"><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/><use xlink:href="#h"/></g>
<g id="j"><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/><use xlink:href="#i"/></g>
</svg>
</textarea>
<p class="report"></p>
<p><button onclick="validateSVG(filecheck1.value, {useElsNested:100})">Check validity</button></p>
<h3>Malicious svg 2</h3>
<textarea id="filecheck2">
<!DOCTYPE testingxxe [ <!ENTITY xml "Hello World!"> ]>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<image height="30" width="30" xlink:href="https://yourimage.com" />
<text x="0" y="20" font-size="20">&xml;</text>
</svg>
</textarea>
<p><button onclick="validateSVG(filecheck2.value)">Check validity</button></p>
<h3>Undesired elements svg 3 (contains js)</h3>
<textarea id="filecheck3">
<svg><text>test</text>
<script>alert('Hello World')</script>
</svg>
</textarea>
<p><button onclick="validateSVG(filecheck3.value)">Check validity</button></p>
<h3>Invalid svg 4 (not parseable)</h3>
<textarea id="filecheck4">
< svg > <text x="0" y="20" font-size="20">
</textarea>
<p><button onclick="validateSVG(filecheck4.value)">Check validity</button></p>
<input type="file" class="inputFile hidden" id="inputFile" accept="image/*">
<img id="imgPreview" style="height:2em;">
<script>
inputFile.addEventListener('change', e => {
handleFiles(e.currentTarget, e.currentTarget.files)
})
inputFile.addEventListener('mouseup', e => {
e.currentTarget.value = '';
imgPreview.src = '';
})
function handleFiles(inputEl, files) {
//delete previous
for (let i = 0; i < files.length; i++) {
readFiles(inputEl, files[i]);
}
}
/**
* define allowed/required elements
* or limits
*/
let allowed = {
//useEls: 10,
//hasPrologue: false,
//hasXmlns: true,
useElsNested: 10000,
hasScripts: false,
hasEntity: false,
fileSizeKB: 200,
isSymbolSprite: false,
isSvgFont: false
};
function readFiles(inputEl, file) {
var reader = new FileReader();
let type = file.type;
let isValid = false;
reader.onload = function(e) {
let data = e.target.result;
if (type === 'image/svg+xml') {
// validate
isValid = validateSVG(data, allowed);
if (isValid) {
let dataUrl = URL.createObjectURL(file);
imgPreview.src = dataUrl;
imgPreview.onload = function() {
URL.revokeObjectURL(dataUrl);
};
}
// not valid delete file
else {
inputEl.value = '';
let errorImg = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 27 48'%3E%3Cpath fill='red' d='M26.16,17.92l-10.44,10.44l10.44,10.44l-2.44,2.44l-10.44-10.44l-10.44,10.44l-2.44-2.44l10.44-10.44l-10.44-10.44l2.44-2.44l10.44,10.44l10.44-10.44Z'%3E%3C/path%3E%3C/svg%3E`;
imgPreview.src = errorImg;
}
} else {
imgPreview.src = data;
}
};
//reader.readAsDataURL(file);
if (type === 'image/svg+xml') {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
}
</script>
根据检索到的数据,您可以定义自定义验证模式以排除某些类型的 svg 文件,例如通过限制:
元素总量
是否允许使用脚本标签
文件大小
等等,并通过在“允许”对象中定义参数(或使用自定义条件的数据)
让允许= { useElsNested: 10000, 有脚本:假, 拥有实体:假, 文件大小KB:200, isSymbolSprite:假, isSvgFont: false };
<use>
引用检测“十亿笑攻击”解析 svg 文件输入后
doc = new DOMParser().parseFromString(markup, "image/svg+xml");
svg = doc.querySelector("svg");
我们可以查询
<use>
元素
let useEls = svg.querySelectorAll("use")
然后我们可以搜索嵌套使用引用:
我们查询
href
或 xlink:href
属性以在另一个循环中查找它们引用的元素。
我们可以设置一个最大限制,例如100个嵌套使用引用元素来加速检测过程
const countUseRefs = (useEls, maxNested = 200) => {
let nestedCount = 0;
//stop loop if number of nested use references is exceeded
for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
let use = useEls[i];
let refId = use.getAttribute("xlink:href")
? use.getAttribute("xlink:href")
: use.getAttribute("href");
refId = refId ? refId.replace("#", "") : "";
//normalize href attributes to facilitate JS selection
use.setAttribute("href", "#" + refId);
let refEl = svg.getElementById(refId);
let nestedUse = refEl.querySelectorAll("use");
let nestedUseLength = nestedUse.length;
nestedCount += nestedUseLength;
// query nested use references
for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
let nested = nestedUse[n];
let id1 = nested.getAttribute("href").replace("#", "");
let refEl1 = svg.getElementById(id1);
let nestedUse1 = refEl1.querySelectorAll("use");
nestedCount += nestedUse1.length;
}
}
fileReport.useElsNested = nestedCount;
return nestedCount;
};
如果嵌套
<use>
引用的总数超过一定限制 - 该文件高度可疑,因此我们通过重置输入值来阻止上传。
这种方法肯定不理想,并且会导致误报。
但是,“预解析”的概念有助于检查 svg 文件是否存在格式错误(或各种不需要的元素)。
PerformanceElementTiming.renderTime
/PerformanceObserver
听起来很有希望实际检测渲染性能 - 至少对于像 svg 过滤器这样的性能“重量级人物”来说是这样。此外,完全渲染/执行潜在恶意代码的想法 - 可能不是一个好主意。
您可以设置文件上传的最大文件大小。这应该可以解决你的问题