如何识别 SVG 是否消耗了所有浏览器内存并捕捉窗口

问题描述 投票:0回答:2

问题

当我尝试上传下面的 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>

javascript svg memory browser
2个回答
3
投票

基于原始文本和解析标记的启发式验证

可能不是最优雅的方式,但由于 svg 是基于 xml 的,
我们可以通过分析文件输入数据来查询潜在的恶意代码在上传之前

  1. 原始文本级别:(regexpattern).test(fileinputString)如果有任何可疑/不需要的元素,例如xml
    [!entity]
  2. 解析标记级别(未渲染!):通过
    new DOMParser().parseFromString(markup, "image/svg+xml")
    解析文件输入数据以查询潜在的恶意或不需要的元素(例如
    <script>
    标签)

示例:分析 svg 进行验证

使用的

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 文件是否存在格式错误(或各种不需要的元素)。

实际测试 JavaScript 中的渲染性能?

除了复杂的服务器端(沙盒)测试概念之外 - 实验性

PerformanceElementTiming.renderTime
/
PerformanceObserver
听起来很有希望实际检测渲染性能 - 至少对于像 svg 过滤器这样的性能“重量级人物”来说是这样。
不幸的是,由于当前的浏览器支持,无法使用。.

此外,完全渲染/执行潜在恶意代码的想法 - 可能不是一个好主意。


-2
投票

您可以设置文件上传的最大文件大小。这应该可以解决你的问题

© www.soinside.com 2019 - 2024. All rights reserved.