?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>