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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
observeIframeNavigation read iframe.contentWindow.location.href once,
outside any guard. For a cross-origin iframe that read throws a
SecurityError, which escaped as an uncaught exception and never reached
the polling loop, whose own read of the same property is already guarded.
The first read is now guarded as well, and the observer returns because it
cannot follow a cross-origin frame anyway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
?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>
Every one of these paths checked a string that the browser reinterprets
afterwards. isSafeUrl now hangs the value on an <a> and reads back
probe.protocol, so the check sees what the browser will see: a pre-parse
test reads "javascript:" as a relative path and passes it, and the
HTML parser then decodes it to "javascript:".
marked passes raw HTML through and emits hrefs unescaped. renderMarkdown
escapes the angle brackets before parsing, parses into an inert DOMParser
document where no script runs and no image loads, and drops anchors and
images whose scheme is not http, https or mailto. Blockquotes and
<autolinks> stop working as a result; neither appears in the configuration.
modalTitle and the alternatives list interpolated subitem.name and
icon.class into innerHTML. Both are built as nodes now. name is a
translatable key, so it arrives from the machine-written catalogues.
The link kept its click handler and its class across popups, because one
anchor serves all of them: a later, unrelated click opened whatever an
earlier popup pointed at, and addEventListener stacked one handler per
open. Both are reset per popup and the handler is assigned, not added.
openIframe guards its own argument. Removing the href alone left the
handler passing the raw URL on, and ?iframe= in the query string reaches
the same sink with no configuration involved at all.
Verified in headless Chromium: decimal and hex character references,
	- and 
-split schemes, reference-style links, raw HTML as a
link's text, and the two name sinks all executed before these changes.
injection.spec.js keeps all fifteen payloads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>