From 05e78821a53492737f9d0954c6a0e5d1734b1efc Mon Sep 17 00:00:00 2001 From: rabail-aamir Date: Tue, 23 Sep 2025 22:39:24 +1000 Subject: [PATCH] Fix: prevent XSS in createModal by building modal via DOM APIs (avoid innerHTML) --- javascript/UI/modal.js | 128 +++++++++++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 37 deletions(-) diff --git a/javascript/UI/modal.js b/javascript/UI/modal.js index ac499ee..9f6f61d 100644 --- a/javascript/UI/modal.js +++ b/javascript/UI/modal.js @@ -1,46 +1,100 @@ "use strict"; -// Thin wrapper around Bootstrap, in case we stop using it later -function createModal(name, title, content, primaryButton, secondaryButton = null){ - - let modal = document.createElement("div"); - - // Create via string template... - let modalString = [ - ''].join("\n"); - - modal.innerHTML = modalString; - - if (secondaryButton != null){ - var sButton = document.createElement("button"); - sButton.classList.add("btn","btn-secondary"); - sButton.innerText = secondaryButton.label; +// helper: create element, set attrs/styles, append children +function elem(tag, attrs = {}, childElems = []){ + let elem = document.createElement(tag); + + for (const [attrName, attrVal] of Object.entries(attrs)){ + if (attrName == 'style'){ + for (const [styleName, styleVal] of Object.entries(attrVal)){ + elem.style[styleName] = styleVal; + } + } else { + elem.setAttribute(attrName, attrVal); + } + } + + // append strings (-> TextNode) or Node objects + elem.append(...childElems); + + return elem; +} + +// parse HTML string into a document body (use with care) +function elemFromText(text) { + return new DOMParser().parseFromString(text, "text/html").body; +} + +// fade out and remove element +function removeFadeOut(el, speed) { + var seconds = speed/1000; + el.style.transition = "opacity " + seconds + "s ease"; + el.style.opacity = 0; + setTimeout(function() { + if (el.parentNode) el.parentNode.removeChild(el); + }, speed); +} + +// createModal: builds a Bootstrap modal safely (no innerHTML) +function createModal(name, title, content, primaryButton, secondaryButton = null) { + const modal = elem("div", { + id: name, + class: "modal fade", + tabindex: "-1", + "aria-hidden": "true", + "aria-labelledby": "exampleModalLabel" + }); + + const dialog = elem("div", { class: "modal-dialog" }); + modal.append(dialog); + + const contents = elem("div", { class: "sk-contents" }); + dialog.append(contents); + + // header (title as text) + const header = elem("div", { class: "sk-header sk-header-indent" }, [ + elem("h2", {}, [document.createTextNode(title)]), + elem("button", { + type: "button", + class: "btn-close btn-close-white", + "data-bs-dismiss": "modal", + "aria-label": "Close" + }) + ]); + contents.append(header); + + // body: string -> TextNode; Node -> append + const bodyNode = elem("div", { class: "sk-contents modal-body" }); + if (typeof content === "string") { + bodyNode.appendChild(document.createTextNode(content)); + } else if (content instanceof Node) { + bodyNode.appendChild(content); + } else { + bodyNode.appendChild(document.createTextNode(String(content))); + } + contents.append(bodyNode); + + // footer and buttons + const footer = elem("div", { class: "sk-header sk-modal-footer" }); + contents.append(footer); + + if (secondaryButton) { + const sButton = elem("button", { class: "btn btn-secondary" }, [ + document.createTextNode(secondaryButton.label) + ]); sButton.onclick = secondaryButton.callback; - modal.getElementsByClassName("sk-modal-footer")[0].appendChild(sButton); + footer.appendChild(sButton); } - if (primaryButton != null){ - var pButton = document.createElement("button"); - pButton.classList.add("btn","btn-success"); - pButton.innerText = primaryButton.label; + + if (primaryButton) { + const pButton = elem("button", { class: "btn btn-success" }, [ + document.createTextNode(primaryButton.label) + ]); pButton.onclick = primaryButton.callback; - modal.getElementsByClassName("sk-modal-footer")[0].appendChild(pButton); + footer.appendChild(pButton); } + // attach and return Bootstrap modal instance document.body.appendChild(modal); - - return new bootstrap.Modal(modal.childNodes[0], {}); + return new bootstrap.Modal(modal, {}); }