Files
homepage.veen.world/app/cypress/e2e/injection.spec.js
Kevin Veen-Birkenbach 857c470c9e fix(iframe): open only configured origins from the iframe query parameter
?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>
2026-09-10 17:35:07 +02:00

286 lines
9.3 KiB
JavaScript

// cypress/e2e/injection.spec.js
describe('Untrusted content in the modal', () => {
const base = {
name: 'Test Item',
identifier: 'ABC123',
icon: { class: 'fa fa-test' },
};
beforeEach(() => {
cy.visit('/');
cy.window().then(win => {
cy.stub(win.navigator.clipboard, 'writeText').resolves();
cy.stub(win, 'alert');
});
});
function open(item = {}) {
cy.window().invoke('openDynamicPopup', { ...base, ...item });
}
describe('markdown rendered into innerHTML', () => {
it('strips a plain script URL', () => {
open({
warning: '[click me](javascript:window.__xss = true)',
info: '![x](data:text/html;base64,PHNjcmlwdD4=)',
});
cy.get('#dynamicModalWarningText').find('a').should('not.exist');
cy.get('#dynamicModalWarningText').should('contain.text', 'click me');
cy.get('#dynamicModalInfoText').find('img').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('strips a script URL hidden behind character references', () => {
open({
warning:
'[a](&#106;avascript:window.__xss=1) [b](&#x6A;avascript:window.__xss=1)',
info: '[c](java&Tab;script:window.__xss=1) [d](java&NewLine;script:window.__xss=1)',
});
cy.get('#dynamicModalWarningText').find('a').should('not.exist');
cy.get('#dynamicModalInfoText').find('a').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('strips a script URL written as a reference-style link', () => {
open({
warning: '[click me][ref]\n\n[ref]: &#106;avascript:window.__xss=1',
});
cy.get('#dynamicModalWarningText').find('a').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('neutralises raw HTML', () => {
open({
warning: '<img src=x onerror="window.__xss = true">',
info: '<a href="javascript:window.__xss = true">x</a>',
});
cy.get('#dynamicModalWarningText').find('img').should('not.exist');
cy.get('#dynamicModalWarningText').should('contain.text', 'onerror');
cy.get('#dynamicModalInfoText').find('a').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('neutralises raw HTML used as the text of a stripped link', () => {
open({ warning: '[<img src=x onerror="window.__xss = true">](javascript:bad)' });
cy.get('#dynamicModalWarningText').find('img').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('keeps a relative link', () => {
open({ warning: 'See [the notes](/#anchor)' });
cy.get('#dynamicModalWarningText')
.find('a')
.should('have.attr', 'href', '/#anchor');
});
it('keeps the text of a link it strips', () => {
open({ warning: '[read this](javascript:window.__xss=1)' });
cy.get('#dynamicModalWarningText').should('contain.text', 'read this');
cy.window().should('not.have.property', '__xss');
});
it('keeps ordinary markdown', () => {
open({ warning: 'See [Matrix](https://matrix.org/) and **mind** this' });
cy.get('#dynamicModalWarningText')
.find('a')
.should('have.attr', 'href', 'https://matrix.org/');
cy.get('#dynamicModalWarningText').find('strong').should('have.text', 'mind');
});
});
describe('values interpolated outside markdown', () => {
it('does not treat an interface string as markup', () => {
cy.window().then(win => {
win.I18N.Open = '<img src=x onerror="window.__xss = true">';
});
open({
alternatives: [
{ name: 'Alt', identifier: 'A', icon: { class: 'fa-alt' } },
],
});
cy.get('#dynamicAlternativesList').find('img').should('not.exist');
cy.get('#dynamicAlternativesList').should('contain.text', 'onerror');
cy.window().should('not.have.property', '__xss');
});
it('falls back to the English source when a string is missing', () => {
cy.window().then(win => {
delete win.I18N;
});
open({
alternatives: [
{ name: 'Alt', identifier: 'A', icon: { class: 'fa-alt' } },
],
});
cy.get('#dynamicAlternativesList button').should('have.text', 'Open');
});
it('renders no placeholder for a missing name', () => {
open({ name: undefined });
cy.get('#dynamicModalLabel').should('not.contain.text', 'undefined');
});
it('does not treat the name or the icon class as markup', () => {
open({
name: '<img src=x onerror="window.__xss = true">',
icon: { class: 'fa" onmouseover="window.__xss = true' },
alternatives: [
{
name: '<img src=y onerror="window.__xss = true">',
identifier: 'ALT1',
icon: { class: 'fa-alt' },
},
],
});
cy.get('#dynamicModalLabel').find('img').should('not.exist');
cy.get('#dynamicModalLabel').should('contain.text', 'onerror');
cy.get('#dynamicAlternativesList').find('img').should('not.exist');
cy.get('#dynamicAlternativesList').should('contain.text', 'onerror');
cy.window().should('not.have.property', '__xss');
});
});
describe('the link the modal offers', () => {
it('drops a URL that uses an unsafe scheme', () => {
open({ url: 'javascript:window.__xss = true', description: 'Bad' });
cy.get('#dynamicModalLinkHref').should('not.have.attr', 'href');
cy.get('#dynamicModalLinkHref').should('have.text', 'Bad');
cy.window().should('not.have.property', '__xss');
});
it('keeps an ordinary URL', () => {
open({ url: 'https://example.com', description: 'Good' });
cy.get('#dynamicModalLinkHref').should(
'have.attr',
'href',
'https://example.com',
);
});
it('keeps a URL that carries surrounding whitespace', () => {
open({ url: ' https://example.com ', description: 'Good' });
cy.get('#dynamicModalLinkHref').should('have.attr', 'href');
});
it('keeps a mailto URL', () => {
open({ url: 'mailto:kevin@veen.world', description: 'Write' });
cy.get('#dynamicModalLinkHref').should(
'have.attr',
'href',
'mailto:kevin@veen.world',
);
});
it('restores the link after a popup whose URL was dropped', () => {
open({ url: 'javascript:window.__xss = true', description: 'Bad' });
cy.get('#dynamicModalLinkHref').should('not.have.attr', 'href');
open({ url: 'https://example.com', description: 'Good' });
cy.get('#dynamicModalLinkHref').should(
'have.attr',
'href',
'https://example.com',
);
});
it('does not let one popup iframe handler outlive it', () => {
open({ url: 'https://a.test/', description: 'A', iframe: true });
cy.get('#dynamicModalLinkHref').should('have.class', 'iframe');
open({ url: 'https://b.test/', description: 'B' });
cy.get('#dynamicModalLinkHref').should('not.have.class', 'iframe');
cy.get('#dynamicModalLinkHref').should($anchor => {
expect($anchor[0].onclick, 'stale click handler').to.equal(null);
});
});
it('opens the current popup URL, not an earlier one', () => {
open({ url: 'https://a.test/', description: 'A', iframe: true });
open({ url: 'https://b.test/', description: 'B', iframe: true });
cy.get('#dynamicModalLinkHref').click();
cy.get('#main')
.find('iframe', { timeout: 4000 })
.should('have.attr', 'src', 'https://b.test/');
});
});
});
describe('Untrusted content reaching the iframe', () => {
const AFTER_THE_FADE = 3000;
it('refuses to open a script URL handed over by the modal', () => {
cy.visit('/');
cy.window().invoke('openDynamicPopup', {
name: 'Bad',
icon: { class: 'fa fa-test' },
url: 'javascript:window.__xss = true',
description: 'Watch',
iframe: true,
});
cy.get('#dynamicModalLinkHref').click({ force: true });
cy.wait(AFTER_THE_FADE);
cy.get('#main').find('iframe').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('refuses a script URL supplied through the query string', () => {
cy.visit('/?iframe=javascript:window.__xss%20%3D%20true');
cy.wait(AFTER_THE_FADE);
cy.get('#main').find('iframe').should('not.exist');
cy.window().should('not.have.property', '__xss');
});
it('still opens a configured iframe target from the query string', () => {
cy.visit('/');
cy.get('a.iframe-link').first().invoke('prop', 'href').then((href) => {
cy.visit(`/?iframe=${encodeURIComponent(href)}`);
cy.get('#main')
.find('iframe', { timeout: AFTER_THE_FADE })
.should('have.attr', 'src', href);
});
});
it('refuses a foreign origin supplied through the query string', () => {
cy.visit('/?iframe=https://example.com/');
cy.wait(AFTER_THE_FADE);
cy.get('#main').find('iframe').should('not.exist');
});
it('does not open a foreign query-string origin in a new tab', () => {
cy.visit('/?iframe=https://example.com/', {
onBeforeLoad(win) {
cy.stub(win, 'open').as('open');
cy.stub(win, 'alert');
},
});
cy.window().then((win) => win.openIframeInNewTab());
cy.get('@open').should('not.have.been.called');
});
});