From 7de3c74f3c11006f21f9b3b36cac38b142a3f5a5 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 23 Sep 2026 10:14:54 +0200 Subject: [PATCH] fix(iframe): stop an absent iframe parameter from framing the page in itself An empty value resolved against window.location.href, so a page without an iframe parameter passed both the scheme check and the origin allowlist carrying its own URL. Every load entered fullscreen and framed the page inside itself, and the navigation observer wrote that URL back into the parameter, nesting it deeper on every poll until the URL ran to kilometres. safeUrl now rejects a missing value, the caller tests the parameter before validating it, and the history entry keeps the URL it was given rather than the resolved one. The regression spec covers the four properties a page without the parameter must have, and fails on each of them when the defect is put back. Co-Authored-By: Claude Opus 5 (1M context) --- app/cypress/e2e/iframe_param_absent.spec.js | 31 +++++++++++++++++++++ app/static/js/iframe.js | 8 +++--- app/static/js/modal.js | 5 +++- 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 app/cypress/e2e/iframe_param_absent.spec.js diff --git a/app/cypress/e2e/iframe_param_absent.spec.js b/app/cypress/e2e/iframe_param_absent.spec.js new file mode 100644 index 0000000..1fe8150 --- /dev/null +++ b/app/cypress/e2e/iframe_param_absent.spec.js @@ -0,0 +1,31 @@ +describe('A page without an iframe parameter', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('stays out of fullscreen', () => { + cy.get('body').should('not.have.class', 'fullscreen'); + }); + + it('frames nothing', () => { + cy.get('#main').find('iframe').should('not.exist'); + cy.url().should('not.include', 'iframe='); + }); + + it('never grows an iframe parameter out of its own URL', () => { + cy.wait(2000); + + cy.url().then((url) => { + expect((url.match(/iframe/g) || []).length, 'iframe parameters').to.equal(0); + }); + }); + + it('treats an absent URL as unsafe', () => { + cy.window().then((win) => { + expect(win.safeUrl(null), 'null').to.equal(null); + expect(win.safeUrl(''), 'empty string').to.equal(null); + expect(win.safeUrl('javascript:alert(1)'), 'script URL').to.equal(null); + expect(win.safeUrl('/de/'), 'relative path').to.equal(`${win.location.origin}/de/`); + }); + }); +}); diff --git a/app/static/js/iframe.js b/app/static/js/iframe.js index efe9e87..422f80c 100644 --- a/app/static/js/iframe.js +++ b/app/static/js/iframe.js @@ -14,8 +14,8 @@ function allowedIframeUrl(url) { // === Auto-open iframe if URL parameter is present === window.addEventListener('DOMContentLoaded', () => { - const paramUrl = allowedIframeUrl(new URLSearchParams(window.location.search).get('iframe')); - if (paramUrl) { + const paramUrl = new URLSearchParams(window.location.search).get('iframe'); + if (paramUrl && allowedIframeUrl(paramUrl)) { currentIframeUrl = paramUrl; enterFullscreen(); openIframe(paramUrl); @@ -83,8 +83,8 @@ function openIframe(url) { // URL-State pushen var newUrl = new URL(window.location); - newUrl.searchParams.set('iframe', target); - window.history.pushState({ iframe: target }, '', newUrl); + newUrl.searchParams.set('iframe', url); + window.history.pushState({ iframe: url }, '', newUrl); }); } diff --git a/app/static/js/modal.js b/app/static/js/modal.js index ea928f4..03c17c8 100644 --- a/app/static/js/modal.js +++ b/app/static/js/modal.js @@ -8,8 +8,11 @@ function t(source) { const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:']; function safeUrl(url) { + if (url == null || String(url) === '') { + return null; + } try { - const parsed = new URL(String(url == null ? '' : url), window.location.href); + const parsed = new URL(String(url), window.location.href); return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null; } catch (error) { return null;