# Arbitrary POST request as victim user from HTML injection in Jupyter notebooks

DOMPurify-preserved data-* attributes in Jupyter notebook HTML hijack GitLab's persistent_user_callout JS to fire arbitrary POSTs as the victim, including admin account creation.

- Payout: hidden
- Severity: high
- Program: GitLab
- Found by: joaxcar
- Disclosed: 2022-05-20
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/1409788
- Library page: https://bugbountylibrary.com/r/h1%3A1409788
- Tags: area:web domain:web impact:ato impact:privesc kind:bounty platform:hackerone sev:high tech:rest technique:js-analysis technique:source-review vuln:access-control vuln:csrf

**Target:** Jupyter notebook HTML output rendering on gitlab.com
**Root cause:** DOMPurify strips Rails UJS data-url/data-method but allows arbitrary data-* attributes; GitLab's deferredInitialisation JS reads data-dismiss-endpoint and feature-id from DOM and issues axios POSTs, so injected HTML can control the request target.
**Steps:**
1. Create a project and add `hack.ipynb` with a display_data output whose `text/html` contains `<div class="js-new-user-signups-cap-reached" data-dismiss-endpoint="https://gitlab.com/api/v4/..." data-defer-links="false" data-feature-id="1"><button class="js-close" style="...full-page overlay...">hack</button></div>`
2. Save and open the notebook preview; refresh until the injected feature-highlight dot renders (deferred init timer)
3. Victim clicks anywhere on the page, triggering the `.js-close` handler which calls `dismiss()` and POSTs to the attacker-controlled `dismissEndpoint`
4. Verify side effect at `/dashboard/todos`; swap endpoint to `https://gitlab.com/api/v4/users?admin=true&email=...&username=...&password=...` to create an admin when an admin visits
**How found:** After the Rails UJS data-* vector was patched by DOMPurify, researcher read main.js deferredInitialisation and audited each init function's source for selector/data-attribute pairs that still drive requests, finding persistent_user_callout's data-dismiss-endpoint plus .js-close trigger.
**Exploitation:** Attacker publishes a notebook with sanitized-but-data-attribute-bearing HTML and an invisible full-page overlay button. When any user (ideally admin) opens the notebook and clicks, GitLab's own JS issues an authenticated POST to the attacker-chosen endpoint, e.g. creating a new admin user or adding todos/invites.
**Escalation:** Targeting an admin visitor escalates from arbitrary POST to full instance admin account creation.
**Insight:** When a sanitizer blocks known dangerous attributes, audit the app's own JS for other data-* attributes that feed into fetch/axios calls; any DOM-controlled endpoint plus a clickable trigger is a CSRF-equivalent even in 'purified' HTML.
