From 8145e26a93cbc72d64e2331314cc4b89af2ff127 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Tue, 7 Jul 2026 15:45:45 +0100 Subject: [PATCH] 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 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 Signed-off-by: Pedro Amorim --- .../prog/en/modules/suggestion/suggestion.tt | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/suggestion/suggestion.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/suggestion/suggestion.tt index 465c384315..6bfb560eb3 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/suggestion/suggestion.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/suggestion/suggestion.tt @@ -1435,15 +1435,34 @@ searchable: true, orderable: true, render: function (data, type, row, meta) { - let node = ""; - if (row.status === "ASKED") node += '%s'.format(escape_str(_("Pending"))); - else if (row.status === "ACCEPTED") node += '%s'.format(escape_str(_("Accepted"))); - else if (row.status === "ORDERED") node += '%s'.format(escape_str(_("Ordered"))); - else if (row.status === "REJECTED") node += '%s'.format(escape_str(_("Rejected"))); - else if (row.status === "CHECKED") node += '%s'.format(escape_str(_("Checked"))); - else if (row.status === "AVAILABLE") node += '%s'.format(escape_str(_("Available"))); - else if (row._strings.status.str) node += '%s'.format(escape_str(row._strings.status.str)); - else node += '%s'.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");