Cronicle: Stored XSS in the admin Activity Log via the full_name field
A non-privileged user sets their own full_name field to an XSS payload that executes in the administrator session when the Activity Log is opened.
Advisory ID: TP-2026-021
Product: Cronicle (multi-server task scheduler and runner with a web UI)
Vulnerability type: Stored cross-site scripting (CWE-79)
CVE: CVE-2026-55562
CVSS 3.1: 9.0 (Critical) · CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
Affected versions: <= 0.9.118
Fixed in: 0.9.119
Vendor advisory: GHSA-vh39-3pcm-2j63
Reported: 13 June 2026
Summary
Cronicle is a multi-server task scheduler with a web UI. The admin Activity Log inlines an account's user-controlled full_name field raw into the row description and writes the assembled markup into the DOM with jQuery .html(), without encoding it. The only input-side defense is a replace(/<.+>/g,'') filter whose dot metacharacter does not match a line feed, so an <img> tag with an embedded line feed passes the filter and stays a valid HTML element. Setting one's own full_name requires only a valid session plus the current password and no privilege, so any authenticated non-admin (or, with free_accounts:true, an anonymous self-registrant) plants the payload. Cronicle serves no Content-Security-Policy, and the payload executes in the administrator's session the moment they open the Activity Log. The result is privilege escalation from non-admin to admin and, through the Shell Plugin, command execution as the Cronicle service account. turingpoint verified the flow and reported it responsibly; the vendor fixed it in 0.9.119.
Root cause
The renderer inlines the attacker-controlled item.user.full_name raw into the row markup and writes it with jQuery this.div.html(html) (htdocs/js/pages/admin/Activity.js:142/146/150/154, sink :243), while the sibling item.description field is escaped via encode_entities (:214/221). The only input-side defense is user.full_name.replace(/<.+>/g,'') in node_modules/pixl-server-user/user.js:485, but the dot metacharacter does not match a line feed, so <img src=x\nonerror=alert(document.domain)> survives the strip and stays a valid HTML element. The full_name field is otherwise validated only against /\S/ (user.js:90), so no further character class blocks the payload. Setting one's own full_name via POST /api/user/update requires only a valid session plus the current password and no privilege (api_update, user.js:421), so any authenticated non-admin (or, with free_accounts:true, an anonymous self-registrant) plants it. Cronicle serves no Content-Security-Policy by default, and the payload fires in the administrator's session the moment they open the requireAdmin-gated Activity Log (lib/api/admin.js:25).
Proof of Concept
As an authenticated non-admin (line feed encoded as JSON \n):
POST /api/user/update HTTP/1.1
Content-Type: application/json
{"session_id":"<non-admin session>","username":"lowuser","full_name":"<img src=x\nonerror=alert(document.domain)>","old_password":"<current password>"}
The line feed inside the <img> tag defeats the /<.+>/g strip, the value is stored byte-for-byte and returned raw in the admin get_activity feed. When the administrator opens the Activity Log, this.div.html() parses the value into a live element whose onerror handler fires in the admin session.
Impact
- Arbitrary JavaScript execution in the administrator's session when they open the Activity Log; no further interaction required.
- Privilege escalation from any authenticated non-admin to admin: new admin accounts and API keys can be created from the victim session.
- Command execution as the Cronicle service account (root in the default container image) through the Shell Plugin.
- Exploitable by an anonymous self-registrant when
free_accounts:trueis set.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.
