From 7353e8d96a22ccf13f88b370c199ed898825b9ca Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 23 Sep 2026 02:10:05 +0200 Subject: [PATCH] fix(iframe): hand the sinks the validated URL, not the parameter The scheme and origin checks lived in a boolean guard in another function, so the raw query parameter still reached the iframe src, the history entry and window.open. The validator now returns the normalised href or null, and every sink consumes only that. Which URLs are accepted does not change: openIframe still checks the scheme alone, because the modal opens configured targets that are not among the page's iframe links. Co-Authored-By: Claude Opus 5 (1M context) --- app/eslint.config.js | 1 + app/static/js/iframe.js | 26 ++++++++++++++------------ app/static/js/modal.js | 10 +++++++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/eslint.config.js b/app/eslint.config.js index 50ea303..35936c4 100644 --- a/app/eslint.config.js +++ b/app/eslint.config.js @@ -21,6 +21,7 @@ const SHARED = { openDynamicPopup: 'readonly', closeAllModals: 'readonly', isSafeUrl: 'readonly', + safeUrl: 'readonly', openIframe: 'readonly', enterFullscreen: 'readonly', exitFullscreen: 'readonly', diff --git a/app/static/js/iframe.js b/app/static/js/iframe.js index 17f96dd..efe9e87 100644 --- a/app/static/js/iframe.js +++ b/app/static/js/iframe.js @@ -2,19 +2,20 @@ let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer; let currentIframeUrl = null; -function isAllowedIframeUrl(url) { - if (!isSafeUrl(url)) { - return false; +function allowedIframeUrl(url) { + const candidate = safeUrl(url); + if (candidate === null) { + return null; } const allowedOrigins = new Set([window.location.origin]); document.querySelectorAll('a.iframe-link[href]').forEach((link) => allowedOrigins.add(link.origin)); - return allowedOrigins.has(new URL(url, window.location.href).origin); + return allowedOrigins.has(new URL(candidate).origin) ? candidate : null; } // === Auto-open iframe if URL parameter is present === window.addEventListener('DOMContentLoaded', () => { - const paramUrl = new URLSearchParams(window.location.search).get('iframe'); - if (paramUrl && isAllowedIframeUrl(paramUrl)) { + const paramUrl = allowedIframeUrl(new URLSearchParams(window.location.search).get('iframe')); + if (paramUrl) { currentIframeUrl = paramUrl; enterFullscreen(); openIframe(paramUrl); @@ -43,7 +44,8 @@ function syncIframeHeight() { // Function to open a URL in an iframe (jQuery version mit 1500 ms Fade) function openIframe(url) { - if (!isSafeUrl(url)) { + const target = safeUrl(url); + if (target === null) { return; } @@ -73,7 +75,7 @@ function openIframe(url) { // Quelle setzen und mit 1500 ms einblenden $iframe - .attr('src', url) + .attr('src', target) .fadeIn(1500, function() { syncIframeHeight(); observeIframeNavigation(); @@ -81,8 +83,8 @@ function openIframe(url) { // URL-State pushen var newUrl = new URL(window.location); - newUrl.searchParams.set('iframe', url); - window.history.pushState({ iframe: url }, '', newUrl); + newUrl.searchParams.set('iframe', target); + window.history.pushState({ iframe: target }, '', newUrl); }); } @@ -148,8 +150,8 @@ document.addEventListener("DOMContentLoaded", function() { */ function openIframeInNewTab() { const params = new URLSearchParams(window.location.search); - const iframeUrl = params.get('iframe'); - if (iframeUrl && isAllowedIframeUrl(iframeUrl)) { + const iframeUrl = allowedIframeUrl(params.get('iframe')); + if (iframeUrl) { window.open(iframeUrl, '_blank'); } else { alert('No iframe is currently open.'); diff --git a/app/static/js/modal.js b/app/static/js/modal.js index 013d143..ea928f4 100644 --- a/app/static/js/modal.js +++ b/app/static/js/modal.js @@ -7,15 +7,19 @@ function t(source) { const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:']; -function isSafeUrl(url) { +function safeUrl(url) { try { const parsed = new URL(String(url == null ? '' : url), window.location.href); - return SAFE_URL_SCHEMES.includes(parsed.protocol); + return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null; } catch (error) { - return false; + return null; } } +function isSafeUrl(url) { + return safeUrl(url) !== null; +} + function iconAndName(item) { const nodes = []; if (item.icon && item.icon.class) {