# LeoCRM Fix-Plan V2 — Gründliche Analyse & Maßnahmen *Erstellt: 2026-07-26 — basierend auf externem Audit + eigener Code-Verifikation* --- ## Zusammenfassung Von 16 zentralen Punkten des externen Audits wurden **alle 16 durch Code-Inspektion verifiziert**. Zusätzlich wurden **5 neue Probleme** gefunden (UploadFile-Bug, Redis-Default-Passwort, exponierte Ports, unauthentifizierter Error-Endpoint, fehlende Security-Headers). **Gesamtstatus:** 4 sauber gefixt · 8 teilweise gefixt · 4 nicht gefixt · 5 neu gefunden = **21 Maßnahmen** --- ## Phase 1: Kritische Release-Blocker (vor Produktivbetrieb) ### B1. Doppelte `get_redis()` entfernen - **Datei:** `app/core/auth.py` Zeilen 53 + 94 - **Problem:** Zweite Definition überschreibt Singleton, erzeugt pro Aufruf neue Verbindung → Connection Leak - **Fix:** Zweite `def get_redis()` (Zeile 94) löschen. Erste Definition (Zeile 53) beibehalten. - **Aufwand:** 5 Min - **Risiko:** Keines — erste Definition ist korrekt ### B2. Plugin-Routen-Registrierung reparieren - **Datei:** `app/main.py` Zeilen 375-416 - **Problem:** Alle Plugin-Routen werden statisch in `create_app()` registriert, unabhängig vom Aktivierungsstatus. Deaktivierte Plugins bleiben erreichbar. Kommentar in Zeile 416 sagt das Gegenteil. - **Fix:** 1. Statische Registrierung aus `create_app()` entfernen 2. In `lifespan()` nur Routen für `active=True` Plugins registrieren 3. `Depends(require_active_plugin("name"))` als zentrale Prüfung ergänzen 4. Bei Deaktivierung: Router entfernen oder 403-Dependency ergänzen - **Aufwand:** 2-3 Std - **Risiko:** Mittel — muss sicherstellen dass keine Route doppelt registriert wird ### B3. UploadFile Route-Registration Bug - **Dateien:** `app/plugins/builtins/dms/routes.py`, `calendar/routes.py`, `mail/routes.py`, `kommunikation/routes.py`, `ai_assistant/routes.py` - **Problem:** FastAPI kann `UploadFile` nicht als Response-Model auflösen → 5 Plugins failen beim Registrieren mit `Invalid args for response field` - **Fix:** `response_model=None` zu allen Endpoints mit `UploadFile`-Rückgabe hinzufügen, oder Return-Type auf `Response`/`dict` ändern - **Aufwand:** 30 Min - **Risiko:** Keines — Routen sind aktuell gar nicht registriert ### B4. DMS-Upload auf echtes Streaming umstellen - **Datei:** `app/plugins/builtins/dms/routes.py` Zeilen 444-472 - **Problem:** Chunks werden in `list[bytes]` gesammelt, dann `b"".join()` → 100MB Datei = 200MB+ RAM. `save_stream()` existiert aber wird nicht benutzt. - **Fix:** ```python async def chunk_generator(): while chunk := await file.read(CHUNK_SIZE): yield chunk await storage.save_stream(storage_path, chunk_generator()) ``` Hash und Größe während des Streams berechnen. - **Aufwand:** 1 Std - **Risiko:** Gering — save_stream() ist bereits implementiert ### B5. Outbox-Worker: Event-Handler registrieren - **Datei:** `app/core/worker.py` `on_startup()` - **Problem:** Worker liest Events aus Outbox, published an lokalen EventBus, aber es sind keine Handler registriert → Events werden als `published` markiert ohne Verarbeitung - **Fix:** 1. In `on_startup()`: Plugin-Event-Handler registrieren (wie in `lifespan()` der API) 2. `webhook_dispatcher._dispatch_event` an EventBus subscriben 3. Plugin-Participant-Handler registrieren - **Aufwand:** 2 Std - **Risiko:** Mittel — muss gleiche Handler wie API-Container registrieren ### B6. Passwort-Reset-Mailjob implementieren - **Dateien:** `app/services/auth_service.py`, `app/core/jobs.py`, `app/core/job_registry.py` - **Problem:** `send_password_reset_email` Job wird gequeued aber nie registriert → Mail wird nicht versendet. Token wird in Logs geschrieben (Zeile 240-241). - **Fix:** 1. `send_password_reset_email` Worker-Funktion implementieren (SMTP/IMAP) 2. Mit `register_job()` registrieren 3. `logger.warning("raw_token for development: %s", raw_token)` entfernen 4. Token nur im Development-Mode loggen, nie in Production - **Aufwand:** 2 Std - **Risiko:** Gering ### B7. Webhook SSRF-Schutz + Secret-Behandlung - **Dateien:** `app/services/webhook_service.py`, `app/schemas/webhook.py` - **Problem:** Kein SSRF-Schutz — User können interne Dienste ansprechen (redis:6379, postgres:5432, 169.254.169.254). Webhook-Secret wird im Response zurückgegeben. - **Fix:** 1. SSRF-Prüfung: DNS auflösen, private IPs blocken (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, ::1) 2. Redirects deaktivieren oder prüfen 3. Protokoll-Allowlist (nur https) 4. `secret` aus `WebhookResponse` entfernen 5. Secret gehasht in DB speichern - **Aufwand:** 3 Std - **Risiko:** Gering ### B8. RLS: Separater DB-Runtime-User - **Dateien:** `docker-compose.yml`, `alembic/versions/0044_db_roles.py` (neu) - **Problem:** `POSTGRES_USER` (crm_user) ist Superuser → umgeht RLS auch mit FORCE. Spätere Tabellen (user_preferences, saved_filters, etc.) haben keine RLS-Policy. - **Fix:** 1. Neue Migration `0044_db_roles.py`: erstellt `crm_runtime` (NOSUPERUSER, NOBYPASSRLS) 2. `crm_runtime` bekommt nur SELECT/INSERT/UPDATE/DELETE Rechte 3. `docker-compose.yml`: API und Worker nutzen `crm_runtime`, Migrationen nutzen `crm_owner` 4. Neue Migration `0045_rls_new_tables.py`: RLS für alle Tabellen mit `tenant_id` die nach 0028 hinzukamen - **Aufwand:** 4 Std - **Risiko:** Hoch — muss bestehende Datenbanken migrieren ohne Datenverlust ### B9. .env.docker.example korrigieren - **Datei:** `.env.docker.example` - **Problem:** Verwendet `AUTH_SECRET` statt `SECRET_KEY` (config.py erwartet `SECRET_KEY`) - **Fix:** `AUTH_SECRET` → `SECRET_KEY` umbenennen - **Aufwand:** 5 Min - **Risiko:** Keines ### B10. Redis-Default-Passwort + exponierte Ports - **Datei:** `docker-compose.yml` - **Problem:** Redis-Passwort default `changeme`, PostgreSQL (5432) und Redis (6379) Ports exponiert - **Fix:** 1. Redis-Passwort als Required-Env ohne Default 2. `ports:` Sektion für DB und Redis entfernen (nur internes Docker-Netzwerk) 3. Falls Debug-Zugriff nötig: nur an 127.0.0.1 binden - **Aufwand:** 15 Min - **Risiko:** Gering — bestehende Setups müssen .env anpassen --- ## Phase 2: Hohe Priorität (kurz nach Release) ### H1. Unauthentifizierter Error-Endpoint absichern - **Datei:** `app/routes/errors.py` - **Problem:** `POST /api/v1/errors` ohne Auth, sendet Daten an Forgejo als öffentliches Issue. Context-Dict kann sensible Daten enthalten. - **Fix:** 1. Context-Felder filtern (keine Tokens, Passwörter, Headers) 2. Forgejo-Issues nur in non-production erstellen 3. Rate-Limit auf IP-Basis (bereits vorhanden, aber in-memory → bei Multi-Worker unzuverlässig) 4. Optional: Auth erforderlich, aber dann funktioniert Frontend-Error-Logging nicht mehr → besser: nur sanitisierte Daten akzeptieren - **Aufwand:** 1 Std ### H2. Rate-Limiter IP-Spoofing - **Datei:** `app/core/rate_limit.py` Zeile 43 - **Problem:** Vertraut `X-Forwarded-For` blind → IP-Spoofing umgeht Rate-Limits - **Fix:** Nur erste IP in X-Forwarded-For verwenden, oder `X-Real-IP` mit Proxy-Validation - **Aufwand:** 30 Min ### H3. CSRF-Middleware Redis-Verbindung - **Datei:** `app/core/middleware.py` Zeile 69 - **Problem:** Erstellt pro unsafe Request neue Redis-Verbindung → Connection Leak - **Fix:** `get_redis()` Singleton verwenden (funktioniert nach B1) - **Aufwand:** 10 Min ### H4. WebSocket Auth + Origin-Verifikation - **Dateien:** `app/plugins/builtins/kommunikation/websocket_manager.py`, `ai_ui_control/websocket_manager.py` - **Problem:** `user_id` wird ohne Auth-Verifikation akzeptiert. Keine Origin-Prüfung bei WS-Upgrade. - **Fix:** 1. Session-Token aus Query-Param oder Header validieren 2. Origin-Header gegen erlaubte Domains prüfen 3. User-ID aus Session ableiten, nicht aus Client-Param - **Aufwand:** 2 Std ### H5. File-Upload-Sicherheit - **Datei:** `app/core/storage.py` - **Problem:** Keine Path-Traversal-Prüfung, keine Type/Size-Limits, `get_url()` leakt Filesystem-Pfade - **Fix:** 1. Filename sanitizen (keine `../`, keine absoluten Pfade) 2. MIME-Type-Allowlist 3. Max-File-Size konfigurierbar 4. `get_url()` gibt relative URL zurück, nicht Filesystem-Pfad - **Aufwand:** 1 Std ### H6. Security-Headers - **Datei:** `app/core/middleware.py` (neu) - **Problem:** Keine Security-Headers (HSTS, X-Content-Type-Options, X-Frame-Options, CSP) - **Fix:** Middleware ergänzen die diese Headers setzt - **Aufwand:** 30 Min ### H7. Migration-Repair für bestehende Installationen - **Datei:** `alembic/versions/0044_repair_contact_migration.py` (neu) - **Problem:** Migrationen 0021 und 0027 wurden nachträglich geändert. Alembic führt sie nicht erneut aus. - **Fix:** 1. Neue Migration die `*_old` Tabellen erkennt und Daten nachmigriert 2. Integritätsprüfung (Anzahl vergleichen) 3. Bei Abweichungen hart abbrechen mit Fehlermeldung - **Aufwand:** 3 Std --- ## Phase 3: Mittlere Priorität ### M1. Passwort-Komplexität - **Datei:** `app/schemas/auth.py`, `app/schemas/user.py` - **Problem:** Min-Length 8 bei Erstellung, Min-Length 1 bei Login. Keine Komplexitäts-Requirements. - **Fix:** Passwort-Validator ergänzen (min 8 Zeichen, 1 Groß, 1 Klein, 1 Zahl) - **Aufwand:** 30 Min ### M2. Login-Response: is_system_admin - **Datei:** `app/routes/auth.py` Zeile 78 - **Problem:** `is_system_admin` Flag in Login-Response leakt interne Rolle - **Fix:** Flag aus Response entfernen oder nur für Admin-User anzeigen - **Aufwand:** 15 Min ### M3. Permission-Cache: Stale Data bei DB-Error - **Datei:** `app/core/permissions.py` Zeile 337 - **Problem:** Bei DB-Error fällt Cache auf stale Daten zurück → widerrufene Rechte bleiben aktiv - **Fix:** Bei DB-Error: Cache invalidieren und 503 zurückgeben statt stale Daten zu nutzen - **Aufwand:** 30 Min ### M4. ENVIRONMENT=development vs SESSION_COOKIE_SECURE=true - **Datei:** `.env` Zeilen 3-4 - **Problem:** Inkonsistent — development deaktiviert Prod-Safety-Checks, aber Cookie ist secure - **Fix:** In .env.docker.example klar dokumentieren: production → `ENVIRONMENT=production` + `SESSION_COOKIE_SECURE=true` - **Aufwand:** 10 Min ### M5. Frontend: Unresolved Items - **Dateien:** `WelcomeDialog.tsx`, `SavedFilterBar.tsx`, `EntityHistoryPanel.tsx`, `TagBadge.tsx`, `TagSelector.tsx` - **Problem:** WelcomeDialog hat `open={false}`. SavedFilterBar/EntityHistoryPanel/TagBadge/TagSelector sind gebaut aber nicht in Seiten integriert. - **Fix:** 1. WelcomeDialog an User-Preferences (onboarding_completed) koppeln 2. SavedFilterBar in ContactsList, Mail, Calendar integrieren 3. EntityHistoryPanel in ContactDetail, Settings integrieren 4. TagBadge/TagSelector in ContactsList, Mail, Calendar integrieren - **Aufwand:** 4 Std ### M6. Frontend-Tests: QueryClientProvider - **Datei:** `frontend/src/test/setup.ts` oder einzelne Tests - **Problem:** ~29 Tests failen mit missing QueryClientProvider - **Fix:** Globalen Test-Wrapper mit QueryClientProvider in setup.ts ergänzen - **Aufwand:** 1 Std --- ## Phase 4: Niedrige Priorität ### L1. document.write() in print.ts - **Datei:** `frontend/src/utils/print.ts` Zeilen 54, 127 - **Problem:** `document.write()` mit DOM-Clone — XSS-Risiko wenn Content nicht sanitized - **Fix:** Statt `document.write()`: `iframe.srcdoc` oder `Blob URL` verwenden - **Aufwand:** 1 Std ### L2. AI UI Control: Unbounded Feedback-Storage - **Datei:** `app/plugins/builtins/ai_ui_control/websocket_manager.py` Zeile 94 - **Problem:** Feedback/Commands unbegrenzt im Memory gespeichert → Memory Exhaustion - **Fix:** Max-Length Queue (z.B. 100 Einträge) mit FIFO - **Aufwand:** 15 Min ### L3. Backup-Strategie dokumentieren - **Problem:** Named Volumes in docker-compose aber keine Backup/Restore-Doku - **Fix:** Backup-Script und Doku ergänzen - **Aufwand:** 2 Std --- ## Implementierungs-Reihenfolge ``` Phase 1 (Release-Blocker): B1 → B3 → B9 → B10 → B2 → B4 → B5 → B6 → B7 → B8 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 5m 30m 5m 15m 3h 1h 2h 2h 3h 4h Gesamt: ~16 Std Phase 2 (Hohe Priorität): H3 → H2 → H6 → H1 → H5 → H4 → H7 Gesamt: ~8 Std Phase 3 (Mittlere Priorität): M4 → M1 → M2 → M3 → M6 → M5 Gesamt: ~6 Std Phase 4 (Niedrige Priorität): L2 → L1 → L3 Gesamt: ~3 Std ``` **Gesamtaufwand: ~33 Std** --- ## Was bereits sauber funktioniert - ✅ Auth-Bypass entfernt (keine X-Internal-Call/X-Tenant-Id/X-User-Id Headers mehr) - ✅ Plugin-Upload/URL-Installation deaktiviert (403) - ✅ Worker in separatem Container - ✅ Metrics adminbeschränkt - ✅ DOMPurify für HTML-Komponenten - ✅ ARQ-Verbindungspool zentralisiert - ✅ Session-Widerruf nach Passwortänderung - ✅ Permission-Cache-Versionierung - ✅ Redis SCAN statt KEYS - ✅ Rabatte von Float auf Numeric - ✅ Event-Outbox als Grundlage vorhanden - ✅ RLS FORCE + WITH CHECK in Migration 0028 - ✅ Migration 0021: Tabellen umbenennen statt löschen - ✅ Frontend: TypeScript typecheck clean (0 errors) - ✅ Frontend: ErrorBoundary, OfflineBanner, ErrorLogger implementiert - ✅ Frontend: Print/PDF mit WeasyPrint funktioniert - ✅ Dockerfile: Multi-stage, non-root User, Healthcheck - ✅ Bcrypt Password-Hashing - ✅ Session-Tokens: secrets.token_urlsafe(32)