5ec1fc9b05
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.
13 KiB
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.pyZeilen 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.pyZeilen 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:
- Statische Registrierung aus
create_app()entfernen - In
lifespan()nur Routen füractive=TruePlugins registrieren Depends(require_active_plugin("name"))als zentrale Prüfung ergänzen- Bei Deaktivierung: Router entfernen oder 403-Dependency ergänzen
- Statische Registrierung aus
- 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
UploadFilenicht als Response-Model auflösen → 5 Plugins failen beim Registrieren mitInvalid args for response field - Fix:
response_model=Nonezu allen Endpoints mitUploadFile-Rückgabe hinzufügen, oder Return-Type aufResponse/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.pyZeilen 444-472 - Problem: Chunks werden in
list[bytes]gesammelt, dannb"".join()→ 100MB Datei = 200MB+ RAM.save_stream()existiert aber wird nicht benutzt. - Fix:
Hash und Größe während des Streams berechnen.
async def chunk_generator(): while chunk := await file.read(CHUNK_SIZE): yield chunk await storage.save_stream(storage_path, chunk_generator()) - Aufwand: 1 Std
- Risiko: Gering — save_stream() ist bereits implementiert
B5. Outbox-Worker: Event-Handler registrieren
- Datei:
app/core/worker.pyon_startup() - Problem: Worker liest Events aus Outbox, published an lokalen EventBus, aber es sind keine Handler registriert → Events werden als
publishedmarkiert ohne Verarbeitung - Fix:
- In
on_startup(): Plugin-Event-Handler registrieren (wie inlifespan()der API) webhook_dispatcher._dispatch_eventan EventBus subscriben- Plugin-Participant-Handler registrieren
- In
- 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_emailJob wird gequeued aber nie registriert → Mail wird nicht versendet. Token wird in Logs geschrieben (Zeile 240-241). - Fix:
send_password_reset_emailWorker-Funktion implementieren (SMTP/IMAP)- Mit
register_job()registrieren logger.warning("raw_token for development: %s", raw_token)entfernen- 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:
- SSRF-Prüfung: DNS auflösen, private IPs blocken (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, ::1)
- Redirects deaktivieren oder prüfen
- Protokoll-Allowlist (nur https)
secretausWebhookResponseentfernen- 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:
- Neue Migration
0044_db_roles.py: erstelltcrm_runtime(NOSUPERUSER, NOBYPASSRLS) crm_runtimebekommt nur SELECT/INSERT/UPDATE/DELETE Rechtedocker-compose.yml: API und Worker nutzencrm_runtime, Migrationen nutzencrm_owner- Neue Migration
0045_rls_new_tables.py: RLS für alle Tabellen mittenant_iddie nach 0028 hinzukamen
- Neue Migration
- Aufwand: 4 Std
- Risiko: Hoch — muss bestehende Datenbanken migrieren ohne Datenverlust
B9. .env.docker.example korrigieren
- Datei:
.env.docker.example - Problem: Verwendet
AUTH_SECRETstattSECRET_KEY(config.py erwartetSECRET_KEY) - Fix:
AUTH_SECRET→SECRET_KEYumbenennen - 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:
- Redis-Passwort als Required-Env ohne Default
ports:Sektion für DB und Redis entfernen (nur internes Docker-Netzwerk)- 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/errorsohne Auth, sendet Daten an Forgejo als öffentliches Issue. Context-Dict kann sensible Daten enthalten. - Fix:
- Context-Felder filtern (keine Tokens, Passwörter, Headers)
- Forgejo-Issues nur in non-production erstellen
- Rate-Limit auf IP-Basis (bereits vorhanden, aber in-memory → bei Multi-Worker unzuverlässig)
- 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.pyZeile 43 - Problem: Vertraut
X-Forwarded-Forblind → IP-Spoofing umgeht Rate-Limits - Fix: Nur erste IP in X-Forwarded-For verwenden, oder
X-Real-IPmit Proxy-Validation - Aufwand: 30 Min
H3. CSRF-Middleware Redis-Verbindung
- Datei:
app/core/middleware.pyZeile 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_idwird ohne Auth-Verifikation akzeptiert. Keine Origin-Prüfung bei WS-Upgrade. - Fix:
- Session-Token aus Query-Param oder Header validieren
- Origin-Header gegen erlaubte Domains prüfen
- 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:
- Filename sanitizen (keine
../, keine absoluten Pfade) - MIME-Type-Allowlist
- Max-File-Size konfigurierbar
get_url()gibt relative URL zurück, nicht Filesystem-Pfad
- Filename sanitizen (keine
- 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:
- Neue Migration die
*_oldTabellen erkennt und Daten nachmigriert - Integritätsprüfung (Anzahl vergleichen)
- Bei Abweichungen hart abbrechen mit Fehlermeldung
- Neue Migration die
- 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.pyZeile 78 - Problem:
is_system_adminFlag 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.pyZeile 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:
.envZeilen 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:
- WelcomeDialog an User-Preferences (onboarding_completed) koppeln
- SavedFilterBar in ContactsList, Mail, Calendar integrieren
- EntityHistoryPanel in ContactDetail, Settings integrieren
- TagBadge/TagSelector in ContactsList, Mail, Calendar integrieren
- Aufwand: 4 Std
M6. Frontend-Tests: QueryClientProvider
- Datei:
frontend/src/test/setup.tsoder 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.tsZeilen 54, 127 - Problem:
document.write()mit DOM-Clone — XSS-Risiko wenn Content nicht sanitized - Fix: Statt
document.write():iframe.srcdocoderBlob URLverwenden - Aufwand: 1 Std
L2. AI UI Control: Unbounded Feedback-Storage
- Datei:
app/plugins/builtins/ai_ui_control/websocket_manager.pyZeile 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)