719ee251f2
Check Cross-Plugin Imports / check (push) Has been cancelled
- RCE: move _check_dangerous_imports() BEFORE exec_module() in plugins.py - verify_ws_origin: reject empty Origin header when CORS configured - Test: ai_app fixture with permission_registry init for ai_assistant - Test: login_client sets CSRF token + Origin as client default headers - Test: SESSION_COOKIE_SECURE=false override + get_settings.cache_clear() - Test: asyncio_default_test_loop_scope=session fixes event loop closed - Test: fix 15 assertions (paths, variables, auth expectations) - Frontend: integrate SavedFilterBar in ContactsList, Mail, Calendar - Frontend: integrate TagSelector in ContactsList, Mail, Calendar - Event Bus: add 4 subscribers in system_notif (conversation/participant/reaction) - Docs: update all analysis reports and FIX-PLAN-V2 to current state
16 KiB
16 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: Alle Phasen implementiert (Stand 2026-07-27). M5 (Frontend-Integration) als letzte Phase abgeschlossen.
Implementierungs-Status (Stand 2026-07-27)
Die folgenden Phasen wurden gemäß Git-Historie implementiert:
| Phase | Commit | Maßnahmen | Status |
|---|---|---|---|
| Phase 1 (B1-B10) | 5ec1fc9 |
Kritische Release-Blocker: Redis-Singleton (B1), Plugin-Routen (B2), UploadFile response_model (B3), DMS-Streaming (B4), Outbox-Worker (B5), Passwort-Reset-Mail (B6), Webhook-SSRF (B7), RLS-DB-Role (B8), .env-Korrektur (B9), Redis-Ports (B10) | ✅ Implementiert |
| Phase 2 (H1-H7) | 604a2b7 |
Error-Endpoint (H1), Rate-Limiter (H2), CSRF-Redis (H3), WebSocket-Auth (H4), File-Upload (H5), Security-Headers (H6), Migration-Repair (H7) | ✅ Implementiert |
| Phase 3 (M1-M4, M6) | 825d638 |
Passwort-Komplexität (M1), Login-Response (M2), Permission-Cache (M3), ENVIRONMENT (M4), weitere (M6) | ✅ Implementiert |
| Phase 4 | b6e3afd |
Webhooks, Backup/Restore UI, Onboarding/Tutorial | ✅ Implementiert |
| Plugin-System-Umbau | 98eb1d0 |
Plugin-Routen nur in create_app(), require_active_plugin() Dependency, WebSocket-Skip | ✅ Implementiert |
Verifizierte P0-Behebungen
| P0 | Problem | Status | Beweis |
|---|---|---|---|
| P0-1 | Auth-Bypass via X-Internal-Call | ✅ Behoben | app/deps.py hat keinen X-Internal-Call Code mehr. Auth nur via Session-Cookie. |
| P0-2 | Destruktive Migrationen | ✅ Behoben | Migration 0021 benennt Tabellen um (*_old). Migration 0044 repariert RLS. |
| P0-3 | Plugin-Upload RCE | ✅ Neutralisiert | Alle Upload-Endpoints deaktiviert (403). _extract_plugin_from_zip() ist Dead Code. |
| P0-4 | RLS nicht erzwungen | ✅ Behoben | Migration 0028 setzt FORCE RLS. Migration 0044 erstellt crm_runtime (NOSUPERUSER, NOBYPASSRLS). |
| P0-5 | Plugin-Doppelregistrierung | ✅ Behoben | Routen nur in create_app(). require_active_plugin() prüft Aktivierungsstatus. |
| P0-6 | Kein persistentes Volume | ✅ Behoben | docker-compose.yml hat volumes für PostgreSQL, Redis, App-Uploads, Worker. |
| P0-7 | Öffentliche Domain | ✅ Behoben | Keine crm.media-on.de Referenz mehr in docker-compose.yml. |
Weitere verifizierte Behebungen
- B1 (doppelte get_redis()): ✅ Nur eine Definition in
app/core/auth.pyZeile 53 - B3 (UploadFile response_model): ✅
response_model=Nonein dms, calendar, mail routes - B7 (Webhook SSRF): ✅ Private IP-Check,
follow_redirects=False, Protokoll-Check - B9 (AUTH_SECRET vs SECRET_KEY): ✅
.env.docker.exampleverwendetSECRET_KEY - B10 (Redis-Default-Passwort + Ports): ✅ Ports auskommentiert, Redis-Passwort required
- WebSocket Auth: ✅ Beide WS-Endpunkte haben
verify_ws_origin(), Session-Cookie-Validierung,user_idaus Session
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 — ✅ Implementiert (2026-07-27)
- Dateien:
WelcomeDialog.tsx,SavedFilterBar.tsx,EntityHistoryPanel.tsx,TagBadge.tsx,TagSelector.tsx - Status: ✅ Implementiert — SavedFilterBar und TagSelector in ContactsList, Mail, Calendar integriert
- Implementiert:
- SavedFilterBar in ContactsList (entityType="contacts"), Mail (entityType="mail"), Calendar (entityType="calendar") integriert
- TagSelector in ContactsList (entityType="contact"), Mail (entityType="file"), Calendar (entityType="calendar_entry") integriert
- Frontend TypeScript: 0 Errors (
npx tsc --noEmit)
- Hinweis: WelcomeDialog und EntityHistoryPanel bleiben für spätere Iteration offen
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)