Skip to content

Bug Issue 1561 cfieldsView Stored XSS

opencode[bot] edited this page Sep 22, 2026 · 3 revisions

Bug fix — Issue #1561: stored XSS in cfieldsView (custom-field name/label + delete action)

Symptom

The modern Custom Fields screen (gui/templates/cfields/cfieldsView.html) rendered custom-field Name and Label column cells with DataTables, which writes cell data via html(). A custom field whose name/label contains markup (e.g. "> <img src=x onerror=alert(73)>) executed the handler on load. The row Actions cell's inline onclick="deleteCf(id,'name')" only escaped single quotes, so a " in the name broke out of the attribute (stored XSS; any user with cfield_management can seed it — every reader of the list is affected).

Root cause

  1. DataTables cells — renderTable() (cfieldsView.html:198-230) pushed cf.label/cf.name unescaped into DataTables row data; DataTables writes cells via html(). Legacy template cfieldsView.tpl:58-59 used Smarty |escape — lost in the Dashio rewrite.
  2. Delete action attribute — inline onclick="deleteCf(id,'<name>')" escaped only single quotes → a " broke out of the attribute.
  3. Server accepts markup by design (api/cfields/index.php:134-135 TRIM-only — legacy parity). XSS is prevented at render time, not by scrubbing stored labels.

Fix

  • Landed with task #950 (b7aaee02e): new escAttr(), applied to the label/name/type cells (cfieldsView.html:213-215); delete icon moved to data-delete-id/data-delete-name (escaped) + delegated $(document).on('click', '.action-btn.danger[data-delete-id]', ...) handler (lines 209-210, 318-320). jQuery.data() decodes the attribute back to raw text for confirm() — a text-only sink.
  • This run (fix/issue-1561): closed the last two unescaped interpolations on the same screen — the type and node <option> builders in loadMeta():
    • line 159: t.name → escAttr(t.name);
    • line 169: n.name → escAttr(n.name) in value + text (mirrors sibling cfieldsAssignView.html:200,207).

Verification (fresh DB, admin, live browser)

  • Hostile name/label render inert — cell HTML "&gt; &lt;img src=x onerror=alert(73)&gt; / evil &lt;b&gt;bold&lt;/b&gt;, td.querySelector('img') === null, no alert fired.

Hostile field renders as inert text

  • Delete confirm shows the raw name as plain text (non-injectable); DELETE 200 removes the row.

Delete confirm — raw name as inert text

  • Type/Node selects populate identically; edit modal title via .text(); create-via-modal works.
  • Event Viewer clean (only log_level 16 NOTICE audit rows; zero Error/Warning).

Files

  • gui/templates/cfields/cfieldsView.html — 2 one-line escapes (fix run; cell/actions hardening already in b7aaee02e).
  • tmp/TLU_Test_Cases.md — Regression — Issue #1561 suite (5/5 PASS).
  • CHANGELOG — KEY BUGFIX (security) entry.

Result

5/5 PASS, Event Viewer clean, issue closed. (Refs #1561)

Clone this wiki locally