# jupyterlab-git extension: Stored XSS leading to RCE

Stored XSS in jupyterlab-git's PlainTextDiff createHeader() renders unsanitized Git filenames via innerHTML, letting a repo committer execute JS and open a terminal for RCE.

- Payout: Research
- Severity: high
- Program: jupyterlab-git
- Found by: jeffwidman
- Disclosed: 2026-06-19
- CVE: CVE-2026-54527
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-f962-v9hr-pfg5
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-f962-v9hr-pfg5
- Tags: area:oss domain:oss impact:ato impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:rce vuln:xss

**Target:** JupyterLab Git extension diff header rendering (PlainTextDiff.ts createHeader)
**Root cause:** createHeader() concatenates Git filenames from rename history into a string assigned to innerHTML without sanitization, so a crafted filename is parsed as HTML.
**Steps:**
1. Create a file with a crafted filename containing a JS payload, e.g. `<img src=x onerror=eval(atob("base64_payload"))>.py`
2. Rename the file in a subsequent commit and push both commits to a shared repository
3. Victim clones/pulls the repo and opens the Git History tab
4. Victim clicks the rename commit, then clicks the renamed file to view the diff
5. createHeader() builds the diff header with the unsanitized filename and assigns it to innerHTML, executing the payload
6. Injected JS reads the `_xsrf` cookie, POSTs to `/api/terminals`, connects via WebSocket, and runs shell commands
**How found:** AWS Security found it during internal security testing; source review of PlainTextDiff.ts createHeader() showed filenames passed to innerHTML.
**Exploitation:** Attacker with commit access pushes a crafted filename plus a rename commit; victim viewing the rename diff triggers JS that reads the _xsrf cookie, opens a terminal via POST /api/terminals, connects over WebSocket and executes arbitrary shell commands, yielding RCE and secret/credential exfiltration.
**Escalation:** XSS chained to JupyterLab terminal API (xsrf cookie + /api/terminals + WebSocket) to escalate from browser JS to full shell RCE.
**Insight:** Diff/rename headers that interpolate filenames are a stored-XSS sink; in JupyterLab, XSS is trivially escalated to RCE because the _xsrf cookie plus /api/terminals and the terminal WebSocket give a shell from any same-origin script.

## Proof of concept (copied exactly from the original report)

**Crafted XSS filename payload**

```
<img src=x onerror=eval(atob("base64_payload"))>.py
```

