ProxyFix defaults x_for to 1, so ProxyFix(app.wsgi_app, x_proto=1) never disabled it: request.remote_addr and the access log were forgeable by any client that reached the app directly. It is x_for=0 now, asserted rather than assumed. A mutation audit over the change set reverted 196 deliberate behaviours and found 47 that no test noticed. This closes the ones that carry damage: - apod_background lost its key check, its transport guard, its status guard and its media-type check without a single test failing. Each one turns a slow or unhappy NASA into a 500 on every page. - Untrusted values reached innerHTML through window.I18N, which the translation backend writes, and the modal's click handlers stacked so a later click opened an earlier popup's URL. - The sync tool could ask for HTML instead of text, translate from "auto" instead of English, run without a timeout, store an empty translation that marks the string done for good, abandon 28 languages because one could not be written, and report success after reaching nothing. - Neither the lint target, the CI jobs, the vendored RTL stylesheet, the documented environment keys, nor any of the four hardenings in scripts/run-e2e.sh was observed by anything. Three of the new tests passed for the wrong reason on their first cut — a mock that answered None whether or not the guard existed, a raise_for_status that was never called, a string that stayed in the file after the mutation. The audit found those too; all 24 reverts now fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
264 lines
8.6 KiB
JavaScript
264 lines
8.6 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: '',
|
|
});
|
|
|
|
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](javascript:window.__xss=1) [b](javascript:window.__xss=1)',
|
|
info: '[c](java	script:window.__xss=1) [d](java
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]: javascript: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 an ordinary URL from the query string', () => {
|
|
cy.visit('/?iframe=https://example.com/');
|
|
|
|
cy.get('#main')
|
|
.find('iframe', { timeout: AFTER_THE_FADE })
|
|
.should('have.attr', 'src', 'https://example.com/');
|
|
});
|
|
});
|