# XSS: `v-safe-html` is not safe enough

GitLab's v-safe-html sanitizer missed Rails' data-disable-with attribute, letting CI job names inject HTML that fires XSS or CSRF-to-admin when a victim clicks.

- Payout: hidden
- Severity: high
- Program: GitLab
- Found by: yvvdwf
- Disclosed: 2022-11-16
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/1579645
- Library page: https://bugbountylibrary.com/r/h1%3A1579645
- Tags: area:web domain:web impact:ato impact:privesc kind:bounty platform:hackerone sev:high technique:source-review vuln:access-control vuln:csrf vuln:xss

**Target:** CI/CD job list error messages rendered via v-safe-html directive
**Root cause:** DOMPurify allowlist in v-safe-html strips data-remote/data-url/data-type/data-method but not data-disable-with, which rails-ujs later renders as raw HTML on click.
**Steps:**
1. Create project with `.gitlab-ci.yml` containing a job name like `1. XSS<a class="fixed-top fixed-bottom text-hide gl-font-size-42 cursor-default" href=# data-disable-with="<img src=x onerror=alert(document.domain)>">`
2. Add a `trigger-xss` job with `dependencies` referencing that job name
3. Wait for jobs to finish, erase the dependency job's logs, then retry `trigger-xss`
4. Error message renders the job name via v-safe-html; click anywhere on the transparent overlay to fire `onerror`
5. With CSP strict-dynamic, instead use a job name containing `<form action=/api/v4/users/<id>?_method=PUT&admin=true method=post><input type=submit class="fixed-top fixed-bottom text-hide cursor-default" style="font-size:10000px" value=Submit>` to force admin escalation on click
**How found:** Compared the DOMPurify attribute denylist in v-safe-html constants against rails-ujs source, noticing data-disable-with was omitted yet still rendered as HTML by rails-ujs on click; then traced where user-controlled strings (CI job names) reach v-safe-html via job error messages.
**Exploitation:** Attacker controls CI job names in a project; victim (including admin) views job list and clicks the transparent full-viewport overlay, executing JS or submitting a forged PUT to /api/v4/users/<id>?admin=true with the victim's session.
**Escalation:** When CSP blocks inline script, swap the img onerror payload for an auto-submitting form targeting the users API to grant admin, turning XSS into privilege escalation.
**Insight:** When a sanitizer uses an attribute denylist, diff it against every attribute the frontend framework's JS reads and re-renders as HTML (rails-ujs data-disable-with, data-confirm, etc.); also check whether allowed style/class can make the injected element a full-viewport transparent click target.
