Bug 42471: (follow-up) prevent XSS in suggestion status

The custom/authorized-value status branch concatenated row.status
directly into a class="..." attribute string. escape_str() only
escapes &, <, > (not quotes), so a status code containing a double
quote could still break out of the attribute.

Build the status <span> via DOM APIs instead, the same way the
reason field was fixed: fixed statuses get a hardcoded class, and
the free-form status code goes into a data-status attribute via
dataset, which the DOM escapes safely regardless of content.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Martin Renvoize 2026-07-07 15:45:45 +01:00 committed by Pedro Amorim
parent 222dac6c3c
commit 8145e26a93
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
1 changed files with 28 additions and 9 deletions

View File

@ -1435,15 +1435,34 @@
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
let node = "";
if (row.status === "ASKED") node += '<span class="status asked">%s</span>'.format(escape_str(_("Pending")));
else if (row.status === "ACCEPTED") node += '<span class="status accepted">%s</span>'.format(escape_str(_("Accepted")));
else if (row.status === "ORDERED") node += '<span class="status ordered">%s</span>'.format(escape_str(_("Ordered")));
else if (row.status === "REJECTED") node += '<span class="status rejected">%s</span>'.format(escape_str(_("Rejected")));
else if (row.status === "CHECKED") node += '<span class="status checked">%s</span>'.format(escape_str(_("Checked")));
else if (row.status === "AVAILABLE") node += '<span class="status available">%s</span>'.format(escape_str(_("Available")));
else if (row._strings.status.str) node += '<span class="status ' + escape_str(row.status) + '">%s</span>'.format(escape_str(row._strings.status.str));
else node += '<span class="status unknown">%s</span>'.format(escape_str(_("Status unknown")));
const status_el = document.createElement("span");
status_el.classList.add("status");
if (row.status === "ASKED") {
status_el.classList.add("asked");
status_el.textContent = _("Pending");
} else if (row.status === "ACCEPTED") {
status_el.classList.add("accepted");
status_el.textContent = _("Accepted");
} else if (row.status === "ORDERED") {
status_el.classList.add("ordered");
status_el.textContent = _("Ordered");
} else if (row.status === "REJECTED") {
status_el.classList.add("rejected");
status_el.textContent = _("Rejected");
} else if (row.status === "CHECKED") {
status_el.classList.add("checked");
status_el.textContent = _("Checked");
} else if (row.status === "AVAILABLE") {
status_el.classList.add("available");
status_el.textContent = _("Available");
} else if (row._strings.status.str) {
status_el.dataset.status = row.status;
status_el.textContent = row._strings.status.str;
} else {
status_el.classList.add("unknown");
status_el.textContent = _("Status unknown");
}
let node = status_el.outerHTML;
if (row.reason) {
const reason_el = document.createElement("div");
reason_el.classList.add("reason");