# Stored XSS in Mermaid when viewing Markdown files

Mermaid directive sets flowchart.htmlLabels to string "false", bypassing label sanitization while still rendering HTML, enabling stored XSS and CSP bypass via pipeline artifacts for account takeover.

- Payout: hidden
- Severity: high
- Program: GitLab
- Found by: saleemrashid
- Disclosed: 2021-10-18
- Write-up quality: 9/10
- Original: https://hackerone.com/reports/1212822
- Library page: https://bugbountylibrary.com/r/h1%3A1212822
- Tags: area:web domain:web impact:ato impact:integrity kind:bounty platform:hackerone sev:high tech:ci-cd technique:chained technique:source-review vuln:misconfiguration vuln:xss

**Target:** Markdown rendering / Mermaid diagrams on gitlab.com
**Root cause:** Mermaid's sanitizer skips escaping when flowchart.htmlLabels is false or the string "false", but the render path checks truthiness of the string "false" (truthy) and renders HTML anyway. GitLab's strict securityLevel is bypassed via a Mermaid init directive.
**Steps:**
1. Create repo on GitLab.com with .gitlab-ci.yml writing `alert(parent.document.querySelector("meta[name=csrf-token]").outerHTML)` to exploit.js as an artifact.
2. Run pipeline, record job ID.
3. In README.md add mermaid block: `%%{init: {"flowchart": {"htmlLabels": "false"}} }%%` with node `A["<iframe srcdoc='<script src=https://gitlab.com/api/v4/projects/<proj>/jobs/<id>/artifacts/exploit.js></script>'"></iframe>"]`.
4. Open README/project overview; alert fires with CSRF token, proving script execution under gitlab.com origin.
**How found:** Read GitLab's Mermaid init config (htmlLabels:false, securityLevel:strict) and Mermaid's secure-key list; noticed directives can't override securityLevel but can set flowchart.htmlLabels. Compared sanitizer check (`=== false || === 'false'`) against render check (truthy `if (config.flowchart.htmlLabels)`), spotting the string "false" discrepancy. Then examined CSP and Workhorse artifact Content-Type handling to find a same-origin script source.
**Exploitation:** Attacker plants a JS artifact in a pipeline (public pipelines or token in URL for private), then injects an iframe srcdoc loading that same-origin artifact as a script. CSP 'self' permits it since Workhorse serves artifacts with JS Content-Type; nonce-based CSP blocks inline scripts but not same-origin src. Script runs as victim, reads CSRF token, enabling full account takeover with no interaction.
**Escalation:** Chained Mermaid sanitizer bypass with CSP bypass via Workhorse artifact Content-Type detection to convert HTML injection into arbitrary JS execution and account takeover.
**Insight:** Look for type-confusion between sanitization guards and render guards: a value compared strictly to false/'false' in one place but truthy-checked in another. Also, same-origin user-upload/artifact endpoints with extension-based Content-Type can satisfy CSP 'self' even when nonces block inline scripts.
