From 64cbff528615632e522cf0587fe5c91ea4ddda83 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Thu, 9 Apr 2026 10:45:43 +0200 Subject: [PATCH] Security hotfixes #1, #2, #6 from audit (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei akute Befunde aus dem Live-System-Audit (Issue #57): - **#1 HIGH** — Resource Exhaustion via öffentlichem POST: slowapi Limiter (in-memory, IP-key) auf /analyze (10/min), /api/analyze-drucksache (10/min) und /api/programme/index (3/min). Verhindert, dass ein unauthentifizierter Client mit einer Schleife die DashScope-Quota oder die CPU des Containers leerziehen kann. Default-Storage reicht solange wir auf einem einzigen Worker laufen. - **#2 MEDIUM** + **#6 MEDIUM** (selber Root-Cause) — XXE/Local-File-Read via WeasyPrint und Stored XSS via Browser-Rendering: alle LLM-getragenen Felder in app/report.py laufen jetzt durch html.escape() bevor sie in die HTML-Template interpoliert werden. format_redline_html escape-first und ersetzt dann die Markdown-Marker durch von uns kontrollierte -Tags. build_matrix_html escaped das aspect-Attribut, sodass ein nacktes " den title="..."-Wert nicht mehr beenden und einen Event- Handler injizieren kann. Toter jinja2-Import in report.py entfernt (war never used, blockierte nur den lokalen Test). - **Tests** — neue tests/test_report.py mit 8 Cases, die direkt die Bug-Klasse verifizieren: nach") + assert "**") + assert "" in out + assert "" +_XXE = '' + + +def _build_xss_assessment() -> Assessment: + return _minimal_assessment( + title=f"Antrag {_XSS}", + gwoe_begruendung=f"Begründung mit {_XSS}", + gwoe_schwerpunkt=[f"Schwerpunkt {_XXE}"], + antrag_zusammenfassung=f"Zusammenfassung mit {_XXE}", + antrag_kernpunkte=[f"Kernpunkt mit {_XSS}"], + staerken=[f"Stärke mit {_XSS}"], + schwaechen=[f"Schwäche mit {_XXE}"], + gwoe_matrix=[ + MatrixEntry(field="A1", label="Lieferant:innen × 1", + aspect=f"Aspekt {_XSS}", rating=2), + ], + wahlprogramm_scores=[ + FraktionScores( + fraktion=f"Fraktion {_XSS}", + wahlprogramm=ProgrammScore(score=5.0, begruendung=f"WP {_XXE}", zitate=[]), + parteiprogramm=ProgrammScore(score=5.0, begruendung=f"PP {_XSS}", zitate=[]), + ), + ], + verbesserungen=[ + Verbesserung( + original=f"Original {_XSS}", + vorschlag=f"Vorschlag **mit {_XSS} markup**", + begruendung=f"Begründung {_XXE}", + ), + ], + ) + + +def test_generate_html_report_escapes_all_llm_payloads(tmp_path: Path): + a = _build_xss_assessment() + out = tmp_path / "report.html" + asyncio.run(generate_html_report(a, out, bundesland="NRW")) + html = out.read_text() + + # Negative: keine rohen Angriffs-Strings + assert "