fix(breadcrumb): prevent XSS in title breadcrumbs – closes #24

This commit is contained in:
Ryan
2025-04-23 22:45:25 -04:00
committed by GitHub
parent 3e1da9c335
commit 58f8485b02
2 changed files with 68 additions and 10 deletions

View File

@@ -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 `<span class="breadcrumb-link" data-folder="…">` 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

View File

@@ -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);