Files
leocrm/FIX-PLAN-V2.md
T
Agent Zero 5ec1fc9b05 Phase 1: Fix all critical release blockers (B1-B10)
B1: Remove duplicate get_redis() — singleton no longer overwritten
B2: Plugin routes now enforce activation status via require_active_plugin()
B3: Fix UploadFile ForwardRef error — remove functools.wraps from wrap_plugin_route
B4: DMS upload uses true streaming via save_stream() instead of RAM accumulation
B5: Worker on_startup registers plugin event handlers + webhook dispatcher
B6: Implement send_password_reset_email job, remove raw token logging
B7: Webhook SSRF protection (IP validation, no redirects), secret removed from response
B8: RLS repair migration 0044 + separate crm_runtime DB user (NOSUPERUSER, NOBYPASSRLS)
B9: Fix .env.docker.example AUTH_SECRET → SECRET_KEY
B10: Remove Redis default password, remove exposed DB/Redis ports

Also: add frontend_url to config, add SMTP settings to .env.docker.example,
update prestart.sh to use MIGRATION_DATABASE_URL for alembic.
2026-07-26 20:45:42 +02:00

13 KiB

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:
    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_SECRETSECRET_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
  • 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)