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:true is 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.