mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-08-24 05:04:33 +00:00
fix(app): stop trusting X-Forwarded-For, and pin what the audit found
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>
This commit is contained in:
@@ -70,7 +70,7 @@ app = Flask(__name__)
|
||||
|
||||
app.jinja_options = {**app.jinja_options, "autoescape": True}
|
||||
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=0, x_proto=1)
|
||||
|
||||
|
||||
def trusted_hosts(raw):
|
||||
|
||||
@@ -72,6 +72,21 @@ describe('Untrusted content in the modal', () => {
|
||||
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' });
|
||||
|
||||
@@ -83,6 +98,40 @@ describe('Untrusted content in the modal', () => {
|
||||
});
|
||||
|
||||
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">',
|
||||
@@ -123,6 +172,12 @@ describe('Untrusted content in the modal', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
@@ -156,6 +211,17 @@ describe('Untrusted content in the modal', () => {
|
||||
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/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user