mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-10 21:26:48 +00:00
?iframe=<url> loaded any http(s) page into the dashboard frame, and "Open in new tab" handed the same query value to window.open unchecked, so a crafted link could show an arbitrary site inside the trusted page. CodeQL flagged both as client-side URL redirection and XSS. The scheme check sat before the fade callback that sets the iframe src, so it did not guard that sink, and isSafeUrl itself assigned the untrusted value to an anchor's href to parse it. isAllowedIframeUrl now requires a safe scheme and an origin that is the page's own or one of the configured .iframe-link targets; both query string entry points check it before openIframe or window.open run. Clicks on configured links and popup entries keep calling openIframe directly, which still rejects unsafe schemes. isSafeUrl parses with new URL instead of a detached anchor. The Cypress case that expected https://example.com/ to open from the query string encoded the redirection, so it now asserts that a configured target opens and that a foreign origin neither loads in the frame nor reaches window.open. make test passes with 109 Cypress tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
158 lines
5.2 KiB
JavaScript
158 lines
5.2 KiB
JavaScript
function t(source) {
|
|
return (window.I18N || {})[source] || source;
|
|
}
|
|
|
|
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
|
|
|
|
function isSafeUrl(url) {
|
|
try {
|
|
const parsed = new URL(String(url == null ? '' : url), window.location.href);
|
|
return SAFE_URL_SCHEMES.includes(parsed.protocol);
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function iconAndName(item) {
|
|
const nodes = [];
|
|
if (item.icon && item.icon.class) {
|
|
const icon = document.createElement('i');
|
|
icon.className = item.icon.class;
|
|
nodes.push(icon, document.createTextNode(' '));
|
|
}
|
|
nodes.push(document.createTextNode(item.name == null ? '' : item.name));
|
|
return nodes;
|
|
}
|
|
|
|
function renderMarkdown(content) {
|
|
const escaped = String(content).replace(/</g, '<').replace(/>/g, '>');
|
|
const parsed = new DOMParser().parseFromString(marked.parse(escaped), 'text/html');
|
|
|
|
parsed.querySelectorAll('a[href]').forEach((anchor) => {
|
|
if (!SAFE_URL_SCHEMES.includes(anchor.protocol)) {
|
|
anchor.replaceWith(...anchor.childNodes);
|
|
}
|
|
});
|
|
parsed.querySelectorAll('img[src]').forEach((image) => {
|
|
if (!SAFE_URL_SCHEMES.includes(image.protocol)) {
|
|
image.replaceWith(image.alt || '');
|
|
}
|
|
});
|
|
|
|
return parsed.body.innerHTML;
|
|
}
|
|
|
|
function openDynamicPopup(subitem) {
|
|
closeAllModals();
|
|
const modalTitle = document.getElementById('dynamicModalLabel');
|
|
modalTitle.replaceChildren(...iconAndName(subitem));
|
|
|
|
const identifierBox = document.getElementById('dynamicIdentifierBox');
|
|
const modalContent = document.getElementById('dynamicModalContent');
|
|
if (subitem.identifier) {
|
|
identifierBox.classList.remove('d-none');
|
|
modalContent.value = subitem.identifier;
|
|
} else {
|
|
identifierBox.classList.add('d-none');
|
|
modalContent.value = '';
|
|
}
|
|
|
|
function toggleBox(boxId, textId, content) {
|
|
const box = document.getElementById(boxId);
|
|
if (content) {
|
|
box.classList.remove('d-none');
|
|
document.getElementById(textId).innerHTML = renderMarkdown(content);
|
|
} else {
|
|
box.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
toggleBox('dynamicModalWarning', 'dynamicModalWarningText', subitem.warning);
|
|
toggleBox('dynamicModalInfo', 'dynamicModalInfoText', subitem.info);
|
|
|
|
const descriptionText = document.getElementById('dynamicDescriptionText');
|
|
if (!subitem.url && subitem.description) {
|
|
descriptionText.classList.remove('d-none');
|
|
descriptionText.innerText = subitem.description;
|
|
} else {
|
|
descriptionText.classList.add('d-none');
|
|
descriptionText.innerText = '';
|
|
}
|
|
|
|
const linkBox = document.getElementById('dynamicModalLink');
|
|
const linkHref = document.getElementById('dynamicModalLinkHref');
|
|
if (subitem.url) {
|
|
linkBox.classList.remove('d-none');
|
|
linkHref.href = subitem.url;
|
|
if (!isSafeUrl(subitem.url)) {
|
|
linkHref.removeAttribute('href');
|
|
}
|
|
linkHref.innerText = subitem.description || t("Open Link");
|
|
linkHref.classList.remove('iframe');
|
|
linkHref.onclick = null;
|
|
if (subitem.iframe) {
|
|
linkHref.classList.add('iframe');
|
|
linkHref.onclick = function(event) {
|
|
event.preventDefault();
|
|
openIframe(subitem.url);
|
|
closeAllModals();
|
|
};
|
|
}
|
|
} else {
|
|
linkBox.classList.add('d-none');
|
|
linkHref.href = '#';
|
|
}
|
|
function populateSection(sectionId, listId, items, onClickHandler) {
|
|
const section = document.getElementById(sectionId);
|
|
const list = document.getElementById(listId);
|
|
list.innerHTML = '';
|
|
|
|
if (items && items.length > 0) {
|
|
section.classList.remove('d-none');
|
|
items.forEach(item => {
|
|
const listItem = document.createElement('li');
|
|
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
|
|
const label = document.createElement('span');
|
|
label.replaceChildren(...iconAndName(item));
|
|
const button = document.createElement('button');
|
|
button.className = 'btn btn-outline-secondary btn-sm';
|
|
button.textContent = t('Open');
|
|
listItem.replaceChildren(label, button);
|
|
button.addEventListener('click', () => onClickHandler(item));
|
|
list.appendChild(listItem);
|
|
});
|
|
} else {
|
|
section.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
populateSection('dynamicAlternativesSection', 'dynamicAlternativesList', subitem.alternatives, openDynamicPopup);
|
|
populateSection('dynamicChildrenSection', 'dynamicChildrenList', subitem.children, openDynamicPopup);
|
|
|
|
const copyButton = document.getElementById('dynamicCopyButton');
|
|
copyButton.onclick = () => {
|
|
modalContent.select();
|
|
navigator.clipboard.writeText(modalContent.value).then(() => {
|
|
alert(t('Identifier copied to clipboard!'));
|
|
});
|
|
};
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('dynamicModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function closeAllModals() {
|
|
const modals = document.querySelectorAll('.modal.show');
|
|
modals.forEach(modal => {
|
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
}
|
|
});
|
|
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
backdrops.forEach(backdrop => backdrop.remove());
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.overflow = '';
|
|
document.body.style.paddingRight = '';
|
|
}
|