Home > database >  How to identify if SVG is consuming all browser's memory and snaps the window
How to identify if SVG is consuming all browser's memory and snaps the window

Time:01-12

Problem

When i try to upload the below SVG, it consumes all memory and eventually main thread freezes and window snap happens.

Steps to reproduce

Run the below snippet and you will see it will snap the result panel, or you can try creating a svg file of this and upload it then also the same thing happens

What I know

This is basically a security threat altogether, commonly refer as SVG Billion Laugh Attack when user uploads such a malicious file and which can eventually consumes browser's huge memory.

Approach to solve

I want to stop such file uploads, which i think is possible if i can recognise such big SVG uploads, someway can track if it's consuming a huge memory say any specific limitation and if that limitation get violated, I can simply stop the user from uploading it.

Thanks in advance

<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>

CodePudding user response:

Heuristic validation based on raw text and parsed markup

Probably, not the most elegant way, but since svg is based on xml,
we can query for potentially malicious code before uploading by analyzing the file input data on:

  1. raw text level: (regexpattern).test(fileinputString) if there are any suspicious/undesired elements like xml [!entity]
  2. parsed markup level (not rendered!): the file input data is parsed via new DOMParser().parseFromString(markup, "image/svg xml") to query potentially malicious or undesired elements (e.g. <script> tags)

Example: analyze svg for validation

The used analyzeSVG(markup, allowed) helper function checks element occurrences like:

  • script tags
  • nested <use> elements
  • total number of elements
  • total number of <use> instances
    etc.

Scroll down to test file uploads - check the console output for detailed feedback.

function validateSVG(markup, allowed = {}) {

  // set defaults
  if (!Object.keys(allowed).length) {
    allowed = {
      useElsNested: 100,
      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");

    // 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,
      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><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"  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,`;
          imgPreview.src = errorImg;
        }

      } else {
        imgPreview.src = data;
      }
    };
    //reader.readAsDataURL(file);
    if (type === 'image/svg xml') {
      reader.readAsText(file);
    } else {
      reader.readAsDataURL(file);
    }
  }
</script>

How it works

Based on the retrieved data you can define a custom validation pattern to exclude certain kinds of svg files e.g. by limiting:

  • the total amount of elements

  • whether script tags are allowed or not

  • filesize
    etc. and by defining the parameters in "allowed" object (or using the data for custom conditions)

    let allowed = { useElsNested: 10000, hasScripts: false, hasEntity: false, fileSizeKB: 200, isSymbolSprite: false, isSvgFont: false };

Detect "billion laugh attack" based on multiplied/nested <use> references

Once the svg file input is parsed

doc = new DOMParser().parseFromString(markup, "image/svg xml");
svg = doc.querySelector("svg");  

we can query <use> elements

let useEls = svg.querySelectorAll("use")  

then we can search for nested use references:
We query href or xlink:href attributes to find their referenced elements in another loop. We can set a maximum limit e.g. 100 nested use referenced elements to speed up the detection process

  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;
  };

If the total amount of nested <use> references exceeds a certain limit – the file is highly suspicious, so we prevent uploading by resetting the input value.

This approach is most certainly not ideal and will result in false-positives.

However, the concept of "pre-parsing" can be helpful to check a svg file for malformed (or all kinds of undesired elements).

Actually testing rendering performance in JavaScript?

Apart from sophisticated server side (sandboxed) testing concepts – the experimental PerformanceElementTiming.renderTime /PerformanceObserver sounds promising to actually detect rendering performance – at least for performance "heavy hitters" like svg filters.
Unfortunately not usable due to current browser support..

Besides, the idea of fully rendering/executing a potentially malicious code – probably not a good idea.

CodePudding user response:

You can set a max file size for file uploads. That should solve you problem

  • Related