# Persistent XSS in Note objects

Project import lets attacker set note_html and cached_markdown_version in project.json, bypassing markdown cache invalidation to store XSS in MR discussions.

- Payout: $4,500
- Severity: high
- Program: GitLab
- Credited: saltyyolk
- Disclosed: 2019-07-19
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/508184
- Library page: https://bugbountylibrary.com/r/h1%3A508184
- Tags: domain:web impact:ato impact:integrity kind:bounty platform:hackerone sev:high tech:ruby technique:chained technique:source-review vuln:misconfiguration vuln:xss

**Target:** GitLab project import / Note objects (merge request discussions)
**Root cause:** CacheMarkdownField invalidation logic checks changed_attributes for 'author'/'project' (but only author_id/project_id change) and deletes 'note' when note_html is present, leaving invalidations empty; cached_html_up_to_date? only compares cached_markdown_version to a predictable constant (917504), so attacker-supplied note_html is trusted.
**Steps:**
1. Export a project containing at least one MR discussion.
2. Edit project.json notes entry: set `note_html` to `<img src="test" onerror="alert(document.domain)">` and `cached_markdown_version` to `917504`.
3. Import the modified project archive.
4. Open the MR discussion; payload executes in victim's session.
**How found:** Read cache_markdown_field.rb invalidation logic, noticed changed_attributes contains author_id/project_id not author/project, and that note is removed from invalidations when note_html changes; then observed latest_cached_markdown_version is a constant 917504 for Notes, making the second check trivially satisfiable.
**Exploitation:** Attacker crafts a malicious project export and imports it (or convinces a victim to import it); the stored note_html renders unsanitized in MR discussions, executing JS in the GitLab origin for any viewer.
**Insight:** When auditing import/export features, look for model attributes that are mass-assignable from serialized JSON but normally derived (like *_html cache fields); check whether cache-invalidation guards compare against attacker-controllable or constant values.
