
CVE-2026-39275 - Stored XSS Leading to Account Takeover in Cockpit CMS
| CVE ID | CVE-2026-39275 |
| Vulnerability | Stored Cross-Site Scripting (XSS) → Account Takeover |
| CWE | CWE-79 |
| Affected | Cockpit CMS <= 2.13.5 |
| Patched | d70dc50 |
| CVSS 3.1 | 8.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N) |
| Reporter | Rohith - Security Consultant @ SecurifyAI |
A stored XSS vulnerability in the content collection list view allows any user with content write access (an API key or an authenticated editor session) to inject arbitrary JavaScript that executes in the browser of any administrator or editor who views the affected collection's item list.
Cockpit's session tokens are fully reachable from JavaScript - the session cookie is set without the HttpOnly flag, and the CSRF JWT is exposed in the global App.csrf variable and the data-csrf attribute on <html>. This lets the injected script read both tokens and replay them externally, defeating CSRF protection and enabling full session hijacking and account takeover.
Field render() outputs are passed to Vue's v-html directive without output encoding.
| File | Line(s) | Role |
|---|---|---|
modules/App/assets/vue-components/fields/field-tags.js | 25 - 34 | Tags renderer - no encoding |
modules/App/assets/vue-components/fields/field-select.js | 26 | Select renderer - no encoding |
modules/Content/views/collection/items.php | 149 | v-html sink |
curl -X POST "http://localhost:8080/api/content/item/posts" \
-H "Content-Type: application/json" \
-H "api-key: USR-<attacker-api-key>" \
-d '{
"data": {
"title": "Innocent Looking Post",
"tags": [
"",
""
],
"body": "Just a regular post.",
"_state": 1
}
}'
An admin or editor navigates to /content/collection/items/posts. The tags renderer returns the stored string, v-html parses it into a real `` element, the bogus src fails, and the onerror handler fires.
Both tokens are now readable - the session cookie via document.cookie and the CSRF JWT via App.csrf / document.documentElement.getAttribute('data-csrf'). A weaponized payload exfiltrates both:

The attacker replays the stolen cookie and CSRF token, gaining full authenticated admin access - and can then create admin accounts, change credentials, or modify content.
A low-privilege editor (or any content write API key holder) escalates to full administrator. The payload is stored and fires for every admin/editor who views the list, making it a persistent, multi-victim compromise.
Primary fix - sanitize render output before it reaches v-html using the existing App.utils.stripTags() helper.
field-tags.js:
value = App.utils.stripTags(value); // sanitize before v-html
field-select.js:
return App.utils.stripTags(value); // sanitize before v-html
Defense in depth - harden the session cookie:
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // over HTTPS
ini_set('session.cookie_samesite', 'Strict');
HttpOnly alone is insufficient - the XSS can still act within the victim's authenticated browser context - so output encoding is the essential fix.
| Date | Event |
|---|---|
| 2026-03-29 | Vulnerability reported to Cockpit CMS maintainers |
| 2026-03-29 | Fix released by maintainers |
| 2026-04-04 | CVE requested |
| 2026-06-23 | CVE-2026-39275 assigned |
<pub date> | Public disclosure |
This material is published for educational and defensive purposes following coordinated disclosure. Use only against systems you own or are explicitly authorized to test.