Regelmäßige Security Audits — initiale Befunde + wiederkehrender Prozess #57
Labels
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: tobias/gwoe-antragspruefer#57
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ziel
Einen wiederkehrenden Security-Audit-Prozess für die GWÖ-Antragsprüfer-Webapp etablieren (nicht nur einmalig). Das Live-System läuft öffentlich auf https://gwoe.toppyr.de ohne Authentication, und mehrere Endpoints triggern teure LLM-Calls oder schreiben in die DB. Ein einmaliger Audit-Pass am 2026-04-09 hat 8 Befunde ergeben, die hier dokumentiert sind. Zusätzlich soll ein wiederkehrender Audit alle 4 Wochen als Issue-Template oder CI-Job eingerichtet werden, damit neue Endpoints nicht durch die Maschen fallen.
Initiale Befunde (Audit 2026-04-09)
1. HIGH — Resource Exhaustion via öffentlichem
/api/analyze-drucksacheDatei:
app/main.py:414–452Endpoint öffentlich, kein Rate-Limit, jeder Call löst 4k-Token-LLM-Job gegen DashScope aus. Angreifer kann massenweise Jobs anstoßen und Quota oder CPU erschöpfen.
Fix-Skizze:
slowapi(FastAPI-Adapter fürlimits) installieren, globales Rate-Limit z.B. 10 req/min/IP auf POST-Routen. ~5min Patch.2. MEDIUM — XXE / Local File Read via WeasyPrint-PDF-Renderer
Datei:
app/report.py:353,370,381,394,401,410,421,427→ WeasyPrint Aufruf Z.471antrag_zusammenfassungundgwoe_begruendungwerden roh aus LLM-Output in HTML interpoliert. Eine Prompt-Injection mit<img src="file:///etc/passwd">oder<link rel=stylesheet href="file:///app/data/gwoe-antraege.db">löst Local File Read im Container-Kontext aus.Fix-Skizze:
html.escape()(stdlib) auf alle LLM-Felder vor String-Interpolation. Alternativ Jinja2-Template mit Auto-Escaping statt F-Strings.3. MEDIUM — Path Traversal auf Drucksache-Parameter
Datei:
app/main.py:286–337drucksache.replace('/', '-')ohne Whitelist-Validation;drucksache=../../etc/passwdbaut auffällige Pfade. Aktuell durch Path-Struktur eingedämmt aber nicht explizit blockiert.Fix-Skizze: Regex
^\d+/\d+(\(neu\))?$als Pflicht-Validation in einer FastAPI-Dependency, fehlerhafte IDs mit 400 ablehnen.4. MEDIUM — PII / LLM-Content im Container-Log
Dateien:
app/analyzer.py:235,308–314,app/main.py:515–516print(f"Failed JSON content (first 500 chars): {content[:500]}")— dumped LLM-Output in stdoutprint(f"ERROR in run_drucksache_analysis for {drucksache}: {e}")— Drucksache-ID + StackDocker-Logs gehen via
docker logsnach außen, sensible Antrags-Inhalte oder Prompt-Material landen ungemaskt im Log.Fix-Skizze: alle
print()durchlogging.getLogger(__name__)ersetzen, LLM-Content nur als Hash + Länge loggen, nicht als Volltext. Bezug zu bestehender Memory-Regel 'stille excepts in Adaptern'.5. MEDIUM — Keine CSRF-Protection auf POST-Endpoints
Datei:
app/main.py:108–165, 414–452, 573–597Drei POST-Routen (
/analyze,/api/analyze-drucksache,/api/programme/index) ohne CSRF-Token. Angreifer-gehostete<form action="https://gwoe.toppyr.de/analyze">startet Job im Browser-Kontext eines Nutzers.Fix-Skizze:
fastapi-csrf-protectMiddleware. Kollidiert mit fehlender Auth — wird vermutlich erst nach Keycloak-Integration sinnvoll.6. MEDIUM — Stored XSS via LLM-Output in HTML-Reports
Datei:
app/main.py:179–192(/result/{job_id}) +app/report.pyHTML-Buildassessment.title,antrag_zusammenfassungetc. werden per F-String ohnehtml.escape()in HTML-Template interpoliert.<script>alert(1)</script>aus LLM-Output wird im Browser des Lesers ausgeführt. Selber Root-Cause wie #2 — derselbe Patch fixt beides.Fix-Skizze: wie #2.
7. LOW-MEDIUM — Search-Query-DoS und stille
shlex.split()-FallbackDateien:
app/main.py:341–366,app/database.py:279Keine Längenbegrenzung auf
q-Parameter;shlex.split()wirftValueErrorbei unbalanced quotes, wird stumm zuquery.split()degradiert (Memory: stille excepts).Fix-Skizze:
qauf max. 200 Zeichen begrenzen,ValueErroralslogger.warningloggen statt verschlucken.8. LOW — Secrets als ENV-Variable statt Mount
Datei:
docker-compose.yml:8–9DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}als ENV-Var → sichtbar indocker inspect, Container-Prozess-Listings.Fix-Skizze: Auf Docker Secrets oder file-mount umstellen. Niedrige Priorität wenn nur ein Container und ein Operator.
Wiederkehrender Audit-Prozess
docs/security-audit-checklist.mdfestschreiben:pip-auditodersafetyüberrequirements.txtlaufen lassen, Befunde im Issue dokumentierengit log --since='4 weeks ago' --diff-filter=A -- 'webapp/app/**.py'für neue Files reviewendocker logs gwoe-antragspruefer --since 4w | grep -i 'error\|exception\|traceback'für stille Fehler-Clusterpip-audit+banditals CI-StepAcceptance Criteria — initial
docs/security-audit-checklist.mdangelegtpip-auditodersafetyeinmalig manuell laufen lassen, Befunde hier ergänzenAcceptance Criteria — wiederkehrend
Bezug
Patch-Status (Commit
64cbff5)/analyze(10/min),/api/analyze-drucksache(10/min),/api/programme/index(3/min), in-memory Limiter mit IP-Keyapp/report.pydurchhtml.escape()Tests
Neue
tests/test_report.pymit 8 Cases —<script>,file://-img,"-Attribut-Breakout, End-to-End-Render mit XSS-Payloads in jedem LLM-Feld. Die**/~~-Marker werden mit-getestet damit das Escape-First-Pattern die Funktion nicht zerstört.Lokal: 85 Unit-Tests grün (77 alt + 8 neu).
Deployment
Die Hotfixes brauchen eine Container-Rebuild auf dem VServer, weil
slowapineu inrequirements.txtist:Nach dem Deploy via
curl -X POST -F drucksache=test https://gwoe.toppyr.de/api/analyze-drucksache×11 verifizieren, dass der 11. Call ein 429 Too Many Requests bekommt.Deployment verifiziert (2026-04-09)
Container-Startup sauber, keine Import-Errors. Live-Test des Rate-Limits gegen
https://gwoe.toppyr.de/api/analyze-drucksache:Rate-Limit greift exakt nach der konfigurierten 10/min-Schwelle. Befunde #1, #2, #6 sind damit live ausgerollt.
Phase A aus Roadmap #59 deployed (Commit
9c70b46). Befunde #3, #4, #7 erledigt. Live verifiziert: bad-Drucksache → 400, ok-Drucksache → 200, 250-Zeichen-Query → 400, 50-Zeichen-Query → 200.Verbleibend: #5 CSRF (hängt an Keycloak), #8 Secrets-Mount (ops change, kein code patch).
Alle 8 Befunde adressiert (2026-04-10)
1a82f82)Wiederkehrender Audit: per Issue-Template oder als Teil jeder groesseren Feature-Session. ADR 0005 dokumentiert die Auth-Entscheidung.
Closing.