From 58f8485b0222b661d86695fd6a60775d50c3ecdc Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 23 Apr 2025 22:45:25 -0400 Subject: [PATCH] =?UTF-8?q?fix(breadcrumb):=20prevent=20XSS=20in=20title?= =?UTF-8?q?=20breadcrumbs=20=E2=80=93=20closes=20#24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++ public/js/folderManager.js | 73 ++++++++++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e68844..67ca46c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,11 @@ - Removed Old CSRF logic that cloned every successful response and parsed its JSON body - Removed Any “soft-failure” JSON peek on non-403 responses - Add missing permissions in `UserModel.php` for TOTP login. +- **Prevent XSS in breadcrumbs** + - Replaced `innerHTML` calls in `fileListTitle` with a new `updateBreadcrumbTitle()` helper that uses `textContent` + `DocumentFragment`. + - Introduced `renderBreadcrumbFragment()` to build each breadcrumb segment as a `` node. + - Added `setupBreadcrumbDelegation()` to handle clicks via event delegation on the container, eliminating per-element listeners. + - Removed any raw HTML concatenation to satisfy CodeQL and ensure all breadcrumb text is safely escaped. ## Changes 4/22/2025 v1.2.3 diff --git a/public/js/folderManager.js b/public/js/folderManager.js index cee7489..036387e 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -104,24 +104,26 @@ export function setupBreadcrumbDelegation() { // Click handler via delegation function breadcrumbClickHandler(e) { + // find the nearest .breadcrumb-link const link = e.target.closest(".breadcrumb-link"); if (!link) return; + e.stopPropagation(); e.preventDefault(); - const folder = link.getAttribute("data-folder"); + const folder = link.dataset.folder; window.currentFolder = folder; localStorage.setItem("lastOpenedFolder", folder); - // Update the container with sanitized breadcrumbs. - const container = document.getElementById("fileListTitle"); - const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder)); - container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")"; - + // rebuild the title safely + updateBreadcrumbTitle(folder); expandTreePath(folder); - document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected")); - const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`); - if (targetOption) targetOption.classList.add("selected"); + document.querySelectorAll(".folder-option").forEach(el => + el.classList.remove("selected") + ); + const target = document.querySelector(`.folder-option[data-folder="${folder}"]`); + if (target) target.classList.add("selected"); + loadFileList(folder); } @@ -332,6 +334,48 @@ function folderDropHandler(event) { }); } +function renderBreadcrumbFragment(folderPath) { + const frag = document.createDocumentFragment(); + const parts = folderPath.split("/"); + let acc = ""; + + parts.forEach((part, idx) => { + acc = idx === 0 ? part : acc + "/" + part; + + const span = document.createElement("span"); + span.classList.add("breadcrumb-link"); + span.dataset.folder = acc; + span.textContent = part; + frag.appendChild(span); + + if (idx < parts.length - 1) { + frag.appendChild(document.createTextNode(" / ")); + } + }); + + return frag; +} + +function updateBreadcrumbTitle(folder) { + const container = document.getElementById("fileListTitle"); + container.textContent = ""; // clear old + + // prefix + container.appendChild( + document.createTextNode(`${t("files_in")} (`) + ); + + // the actual crumbs + container.appendChild( + renderBreadcrumbFragment(folder) + ); + + // closing paren + container.appendChild( + document.createTextNode(")") + ); +} + /* ---------------------- Main Folder Tree Rendering and Event Binding ----------------------*/ @@ -421,7 +465,16 @@ export async function loadFolderTree(selectedFolder) { localStorage.setItem("lastOpenedFolder", window.currentFolder); const titleEl = document.getElementById("fileListTitle"); - titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")"; + titleEl.textContent = ""; + + titleEl.appendChild( + document.createTextNode(t("files_in") + " (") + ); + + const breadcrumbFragment = renderBreadcrumbFragment(window.currentFolder); + titleEl.appendChild(breadcrumbFragment); + + titleEl.appendChild(document.createTextNode(")")); setupBreadcrumbDelegation(); loadFileList(window.currentFolder);