Files
leocrm/FIX-PLAN.md
T
Agent Zero 727d86614e Security fixes: P0-P2 complete (22 fixes)
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed
P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK
P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal

8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
2026-07-25 21:03:46 +02:00

522 lines
21 KiB
Markdown

# LeoCRM — Umfassender Fix-Plan
> Erstellt: 2026-07-25
> Quellen: Externes Audit (geprüft), eigene Code-Inspektion, Coolify-Deployment-Prüfung
---
## P0 — Sofort blockierend (vor jeder Nutzung)
### P0-1: Authentifizierungs-Bypass entfernen
**Problem:** `app/deps.py` akzeptiert `X-Internal-Call: true` mit `X-Tenant-Id` und `X-User-Id` Headern. Keine Signatur, kein Token, keine IP-Beschränkung. `except (ValueError, Exception): pass` verschleiert Fehler.
**Datei:** `app/deps.py:37-58`
**Maßnahme:**
- Header-Authentifizierung komplett entfernen
- Für interne Service-Kommunikation: dedizierte Service-Accounts mit kurzlebigen signierten Tokens (JWT mit `aud`, `iss`, `sub`, `tenant_id`, `exp`)
- Separate interne API oder mTLS
- Keine Übernahme beliebiger `user_id` aus einem Header
- Audit-Logging jeder Delegation
- `except (ValueError, Exception): pass` ersetzen durch spezifisches Exception-Handling mit Logging
**Aufwand:** 2-4 Stunden
---
### P0-2: Destruktive Migrationen ersetzen
**Problem:**
- `alembic/versions/0021_unified_contacts.py`: `DROP TABLE` ohne Datenübernahme
- `alembic/versions/0027_unify_company_to_contact.py`: `company_id` wird gelöscht ohne Datenübernahme; Downgrade ändert pauschal alle `entity_type='contact'` zurück zu `'company'`
- `migration_0021.sql` im Projekt-Root: konkurrierender Migrationsweg, manipuliert `alembic_version` direkt
**Dateien:**
- `alembic/versions/0021_unified_contacts.py`
- `alembic/versions/0027_unify_company_to_contact.py`
- `migration_0021.sql` (löschen)
**Maßnahme:**
1. `migration_0021.sql` löschen
2. Migration 0021 durch echte Transformationsmigration ersetzen:
- Alte Tabellen umbenennen (`_old` suffix), nicht löschen
- Daten mit `INSERT ... SELECT` übertragen
- Anzahl, Checksummen und Plausibilität vor/nach der Migration vergleichen
- Alttabellen erst in späterer Migration entfernen
3. Migration 0027 korrigieren:
- `company_id` Werte vor Drop in `contact_id` übertragen
- Downgrade: nur Datensätze zurückändern, die ursprünglich `'company'` waren (Tracking-Spalte oder separate Tabelle)
4. Automatisierten Upgrade-Test von jeder unterstützten Version auf `head` einführen
5. Migrationen gegen reale anonymisierte DB-Kopien testen
**Aufwand:** 4-8 Stunden
---
### P0-3: Plugin-Upload und URL-Installation deaktivieren
**Problem:** `app/routes/plugins.py` führt `spec.loader.exec_module(module)` aus **bevor** die Sicherheitsprüfung läuft. Das ist Remote Code Execution. Weitere Probleme: unzureichende ZIP-Traversal-Prüfung, kein Symlink-Check, keine ZIP-Bomb-Prävention, SSRF bei URL-Installation, Plugin wird in laufenden Container kopiert.
**Datei:** `app/routes/plugins.py:347-354` (`_extract_plugin_from_zip`)
**Maßnahme:**
1. **Sofort:** Upload- und URL-Installationsendpunkte (`/upload`, `/install-url`) deaktivieren oder entfernen
2. **Langfristig — Vertrauensmodell:**
- Nur signierte Plugin-Artefakte aus einer Allowlist
- Plugin-Code wird vor der Ausführung auf Signatur geprüft
3. **Langfristig — Isolationsmodell:**
- Plugin-Ausführung in separaten Containern mit minimalen Rechten
- Versionierte Plugin-API
4. ZIP-Traversal-Prüfung korrigieren: `os.path.abspath` gegen Base-Dir prüfen nach Extraction
5. Symlink-Check hinzufügen
6. Entpackungsgrößen-Limit (Anzahl Dateien + Gesamtgröße)
7. URL-Download: Redirects verbieten, interne IP-Ranges blockieren, Streaming statt RAM
**Aufwand:** Sofort-Deaktivierung 30 Min; Langfristig 2-3 Tage
---
### P0-4: Mandantentrennung (RLS) reparieren
**Problem:**
- `alembic/versions/0015_rls_policies.py`: Kein `FORCE ROW LEVEL SECURITY`, kein `WITH CHECK`
- Tabellen-Owner umgeht RLS
- Plugin-Tabellen nicht in RLS-Liste
- `TenantMixin` Docstring behauptet ORM-Autofilterung, die nicht existiert
- `app/core/tenant.py` hat nur manuelle `apply_tenant_filter()` Funktion
- `contactpersons` hat `tenant_id` aber FK auf `contacts.id` ohne Tenant-Bedingung → Cross-Tenant-FK möglich
**Dateien:**
- `alembic/versions/0015_rls_policies.py`
- `app/core/db/__init__.py` (TenantMixin Docstring)
- `app/core/tenant.py`
- Neue Migration für FORCE + WITH CHECK
**Maßnahme:**
1. Neue Migration: `ALTER TABLE ... FORCE ROW LEVEL SECURITY` für alle Tenant-Tabellen
2. Policies mit `USING` und `WITH CHECK` neu erstellen
3. Separater DB-Migrationsowner; Runtime-User ohne Owner- oder Bypass-RLS-Rechte
4. RLS für alle mandantenbezogenen Tabellen, einschließlich Plugin-Tabellen
5. CI-Test: Cross-Tenant-Lese- und Schreibversuche
6. Composite-Integrität: eindeutiges `(tenant_id, id)` und FK auf `(tenant_id, contact_id)`
7. `TenantMixin` Docstring korrigieren: Autofilterung existiert nicht
8. Zentralen Query-/Repository-Mechanismus einführen statt freiwilliger Tenant-Filter
9. Später neu erstellte Tabellen automatisch erfassen (Event-Listener oder CI-Check)
**Aufwand:** 1-2 Tage
---
### P0-5: Plugin-System Doppelregistrierung beheben
**Problem:**
- `app/main.py` `create_app()` registriert alle Plugin-Routen unabhängig vom Aktivierungsstatus
- `lifespan()` registriert dieselben Routen nochmal → Doppelregistrierung
- `lifespan()` auto-installiert und auto-aktiviert alle Builtins bei jedem Start
- Deaktivierte Plugins werden reaktiviert
- `registry._plugins` wird direkt zugegriffen (private Feld)
- Migrationsfehler werden nur geloggt, Aktivierung wird trotzdem versucht
- 204 direkte Cross-Imports zwischen Built-in-Plugins
**Datei:** `app/main.py:317-330` und `app/main.py:112-165`
**Maßnahme:**
1. Routen **einmalig** beim Prozessstart registrieren — entweder in `create_app()` ODER in `lifespan()`, nicht beides
2. Aktivierungsstatus vor dem Router-Aufbau laden und respektieren
3. Keine dynamische Änderung von FastAPI-Routen während des Betriebs
4. Aktivierung/Deaktivierung erfordert kontrollierten Neustart
5. Fehlgeschlagene Migration blockiert den Start (nicht nur loggen)
6. Core-Module und optionale Module klar trennen
7. Kein Zugriff auf `registry._plugins` — öffentliche API verwenden
8. Plugin-Abhängigkeiten über deklarierte Contracts prüfen
9. **Langfristig:** Cross-Imports reduzieren — öffentliche Schnittstellen statt direkter Modell-Imports
**Aufwand:** 1 Tag für Doppelregistrierung; Cross-Import-Reduktion 1-2 Wochen
---
### P0-6: Persistent Volume für Coolify-Deployment
**Problem:** Der laufende Container hat **keine Volume-Mounts** (`[]`). `/data/storage` ist nicht persistent. Alle hochgeladenen Dateien (DMS, Attachments, Bilder) gehen bei jedem Redeployment verloren. Plugin-Dateien in `app/plugins/builtins/` überleben keinen Neustart.
**Gefunden in:** Coolify-Container-Inspect (live)
**Maßnahme:**
1. In Coolify persistentes Volume für `/data/storage` konfigurieren
2. Alternativ: S3-kompatiblen Object Storage verwenden (`.env.example` hat bereits `STORAGE_BACKEND=s3` Support)
3. Plugin-Dateien nicht in Container-Filesystem kopieren — separate Plugin-Registry mit DB-basierter Konfiguration
**Aufwand:** 1-2 Stunden (Volume in Coolify konfigurieren)
---
### P0-7: App von öffentlicher Domain nehmen
**Problem:** Die App läuft unter `https://crm.media-on.de` und ist öffentlich erreichbar — mit allen P0-Schwachstellen (Auth-Bypass, Plugin-RCE, XSS, etc.).
**Gefunden in:** Coolify-Deployment-Prüfung
**Maßnahme:**
1. **Sofort:** App von öffentlicher Domain nehmen oder IP-Whitelist/Basic Auth vorschalten
2. Mindestens P0-1 (Auth-Bypass) und P0-3 (Plugin-Upload) beheben bevor wieder öffentlich
3. Alternativ: VPN/Tunnel-Zugang statt öffentliche Domain
**Aufwand:** 30 Minuten
---
## P1 — Vor Nutzung realer Kundendaten
### P1-1: Benutzer- und Mandantenmodell bereinigen
**Problem:**
- `User` hat `tenant_id`, `role`, `role_id` — gleichzeitig existiert `UserTenant` mit `tenant_id`, `role_id`, `is_default`
- Zwei Quellen der Wahrheit für Mandantenzugehörigkeit und Rollen
- `login()` sucht nur nach `email` mit `scalar_one_or_none()` → crasht bei mehreren Treffern (gleiche E-Mail in mehreren Mandanten)
- `tenant_slug` Parameter in `login()` wird von Login-Route nicht übergeben
- `TenantService.list_tenant_users()` sucht über `User.tenant_id` und ignoriert N:M-Mitgliedschaften
**Dateien:**
- `app/models/user.py`
- `app/services/auth_service.py:30-80`
- `app/routes/auth.py`
**Maßnahme:**
1. `users.email` global eindeutig machen (nicht `(tenant_id, email)`)
2. `User.tenant_id` und `User.role`/`User.role_id` entfernen
3. `tenant_memberships` als einzige Quelle: `tenant_id`, `user_id`, `role_id`, `status`, `is_default`
4. `login()` mit `tenant_slug` verknüpfen oder Default-Tenant verwenden
5. `TenantService.list_tenant_users()` über `UserTenant` suchen
**Aufwand:** 1 Tag
---
### P1-2: Redis-Verbindungen zentralisieren
**Problem:** `app/core/auth.py:49-51` erstellt pro Aufruf einen neuen Redis-Client. Kein Pool, kein Close. Dasselbe bei `enqueue_job()` für ARQ-Pools. Folgen: Connection-Lecks, Socket-Erschöpfung, instabiles Verhalten unter Last.
**Datei:** `app/core/auth.py:49-51`, `app/core/worker.py` (enqueue_job)
**Maßnahme:**
1. Redis-Client einmal im Application-Lifespan initialisieren
2. Bei Shutdown schließen
3. Über Dependency Injection verteilen
4. ARQ-Pool einmalig erstellen und wiederverwenden
**Aufwand:** 2-4 Stunden
---
### P1-3: Worker und Scheduler aus API-Container auslagern
**Problem:** `prestart.sh` startet ARQ-Worker im Hintergrund und Uvicorn als PID 1. Worker-Tod wird nicht erkannt. Worker und API konkurrieren um Ressourcen. Keine separate Skalierung. Cron-Jobs können bei mehreren Replikas mehrfach ausgeführt werden.
**Datei:** `prestart.sh`
**Maßnahme:**
1. Worker in separaten Container auslagern
2. Scheduler in separaten Container mit verteilter Lock-/Leader-Election
3. Idempotente Jobs
4. Heartbeat mit Zeitstempel
5. Dead-Letter-/Failed-Job-Strategie
6. Retry-Policy pro Jobtyp
7. Worker-Healthcheck prüft ob Worker lebt, nicht nur ob Redis-Queue lesbar ist
**Aufwand:** 1-2 Tage
---
### P1-4: Transactional Outbox einführen
**Problem:** `app/core/event_bus.py` ist rein speicherbasiert. Events verschwinden bei Prozessabsturz, Neustart, mehreren Replikas, Handler-Fehlern. `asyncio.gather(..., return_exceptions=True)` sammelt Fehler ohne Behandlung.
**Datei:** `app/core/event_bus.py`
**Maßnahme:**
1. Transactional Outbox in PostgreSQL
2. Worker verarbeitet Outbox-Einträge
3. Inbox/Idempotency-Key auf Konsumentenseite
4. Retry und Dead Letter
5. Events versionieren
6. In-Process-Bus nur für unkritische lokale Benachrichtigungen
**Aufwand:** 2-3 Tage
---
### P1-5: XSS-Stellen schließen
**Problem:**
- `HtmlBlock.tsx`: Regex-Sanitizer + `dangerouslySetInnerHTML` — HTML lässt sich nicht sicher mit Regex sanitizen
- `SignatureManager.tsx:201`: `dangerouslySetInnerHTML={{ __html: sig.body_html }}` **ohne jegliche Sanitization**
- `ActionCardBlock.tsx:21-28`: `window.open(action.action)` ohne URL-Validierung — `javascript:`-URLs möglich
- Mail-Service: `body_html_sanitized = body_html` ohne Sanitizer an manchen Stellen
**Dateien:**
- `frontend/src/components/comm/blocks/HtmlBlock.tsx`
- `frontend/src/components/mail/SignatureManager.tsx`
- `frontend/src/components/comm/blocks/ActionCardBlock.tsx`
- Mail-Service (body_html_sanitized)
**Maßnahme:**
1. Serverseitig konsequent `nh3` verwenden
2. Frontend zusätzlich `DOMPurify` als zweite Barriere
3. Keine selbst gebauten Regex-Sanitizer
4. Nur `https:` und kontrollierte interne Pfade erlauben
5. Strikte Content Security Policy ohne `unsafe-inline`
6. Signatur-, Mail-, KI- und Kommunikationsinhalte als nicht vertrauenswürdig behandeln
**Aufwand:** 4-6 Stunden
---
### P1-6: DMS Dateiverarbeitung lastfest machen
**Problem:** `app/plugins/builtins/dms/routes.py` liest die komplette Datei in RAM (`content = await file.read()`). Max 100 MB. Bei 10 parallelen Uploads mehrere GB RAM. Kein Virenscan, kein Content-Hash, keine Dublettenerkennung, keine Tenant-Quotas, kein Versionierungsmodell, kein Garbage Collector für physische Dateien nach Soft Delete. `storage_path` wird an Frontend ausgegeben. Benutzerdateiname direkt in Content-Disposition.
**Datei:** `app/plugins/builtins/dms/routes.py:421-436`
**Maßnahme:**
1. Chunked Streaming direkt in Object Storage
2. Maximale Größe auf Proxy- und Anwendungsebene
3. SHA-256 Content-Hash
4. Malware-Scan
5. Quotas pro Tenant
6. Versionierte Metadaten
7. Garbage Collector für physische Dateien nach Soft Delete
8. `storage_path` nicht an Frontend ausgeben
9. Benutzerdateiname sanitizen vor Content-Disposition
10. Synchronen MinIO-Client aus `async def` entfernen
**Aufwand:** 1-2 Tage
---
### P1-7: Berechtigungssystem vereinheitlichen
**Problem:**
- Legacy-Rollenstrings (`admin`/`editor`/`viewer`) + neue Rollen mit `role_id` + Gruppen + Allow/Deny + Feldrechte + `is_system_admin` + globale Write-Hilfsrechte
- `permission_version` wird gespeichert, beim Cache-Lesen aber nicht geprüft
- Cache-Invalidierung verwendet `redis.keys()` — blockiert Redis bei großen Datenmengen
- Feldrechte mehrerer Gruppen werden per `dict.update()` überschrieben (last-write-wins)
- `viewer` erhält `user_preferences:write`
- `require_write()` erlaubt `*:write` oder `*:create` (zu breit)
- `db.rollback()` bei Permission-Fehler setzt fremde Transaktionsarbeit zurück
**Datei:** `app/core/permissions.py`, `app/deps.py`
**Maßnahme:**
1. Nur noch Capability-basierte Berechtigungen (`contacts.read`, `contacts.create`, etc.)
2. Keine generische `require_write`-Freigabe
3. Alte Rollenlogik entfernen
4. Feldrechte deterministisch nach "strengstes Recht gewinnt" zusammenführen
5. `permission_version` beim Cache-Lesen prüfen
6. `redis.keys()` ersetzen durch `redis.scan()` oder gezielte Cache-Key-Invalidierung
7. `db.rollback()` nur in eigenen Transaktionskontext
**Aufwand:** 1-2 Tage
---
### P1-8: Password Reset funktionsfähig machen
**Problem:** `request_password_reset()` erstellt ein Token, speichert es in der DB, sendet es aber nicht. Nicht einmal geloggt. Die Variable `raw_token` wird nach Erstellung ignoriert. Die Route sagt "a reset link has been sent" — das ist fachlich falsch. Nach Passwortwechsel werden bestehende Sessions nicht widerrufen.
**Datei:** `app/services/auth_service.py:159-200`
**Maßnahme:**
1. Reset-Mail über echte Queue verschicken (ARQ-Worker)
2. Token nur einmal verwendbar
3. Alle Sessions des Benutzers nach Passwortänderung widerrufen
4. Sicherheitsereignis protokollieren
5. Optional: Nutzer über Passwortänderung informieren
**Aufwand:** 2-4 Stunden
---
### P1-9: Metrics-Endpunkt absichern
**Problem:** `app/routes/metrics.py` sagt "admin-only" im Docstring, verwendet aber nur `get_current_user` statt `require_admin`. Jeder angemeldete Benutzer kann Prometheus-Metriken abrufen.
**Datei:** `app/routes/metrics.py`
**Maßnahme:**
1. `require_admin` oder `require_permission("system:metrics")` verwenden
2. Alternativ: internes Netzwerk, Reverse-Proxy-Allowlist, dedizierten Monitoring-Token oder mTLS
**Aufwand:** 30 Minuten
---
### P1-10: Coolify-Dokumentation korrigieren
**Problem:**
- `COOLIFY_SETUP.md` Abschnitt 6 dokumentiert `/health` als Healthcheck-Pfad — die App hat nur `/api/v1/health`. `/health` liefert nur die SPA `index.html` (Catch-All).
- `COOLIFY_SETUP.md` listet `JWT_ALGORITHM` und `JWT_EXPIRY_HOURS` — werden von der App nicht verwendet.
- `CORS_ORIGINS` in Coolify ohne `:443``COOLIFY_SETUP.md` sagt explizit Port ist mandatory.
**Dateien:** `COOLIFY_SETUP.md`, `docs/deployment-guide.md`
**Maßnahme:**
1. Healthcheck-Pfad in Doku auf `/api/v1/health` korrigieren
2. JWT-Variablen aus Doku entfernen oder App auf JWT umstellen
3. `CORS_ORIGINS` in Coolify auf `https://crm.media-on.de:443` setzen
4. `docker-compose.yml` Healthcheck auf `/api/v1/health` korrigieren
5. `docker-compose.yml` Redis-Service hinzufügen
6. `docker-compose.yml` `REDIS_URL` setzen
7. `docker-compose.yml` persistentes Volume für `/data/storage`
8. `docker-compose.yml` `SESSION_COOKIE_SECURE=true` für Production
9. `docker-compose.yml` `STORAGE_PATH=/data/storage` setzen
10. `config.py` Default `storage_path` von `/tmp` auf `/data/storage` ändern
11. `config.py` Default `session_cookie_secure` auf `True` ändern (Production-Default)
12. `config.py` Startup-Validierung: `ENVIRONMENT=production` + `session_cookie_secure=False` → harter Abbruch
**Aufwand:** 2-3 Stunden
---
### P1-11: Cross-Tenant referenzielle Integrität
**Problem:** `contactpersons` hat `tenant_id` aber `contact_id` FK referenziert nur `contacts.id` ohne Tenant-Bedingung. Die DB verhindert nicht, dass ein Contactperson-Datensatz aus Mandant A auf einen Kontakt aus Mandant B zeigt.
**Datei:** `alembic/versions/0021_unified_contacts.py` (contactpersons Tabelle)
**Maßnahme:**
1. Composite-FK: `(tenant_id, contact_id)` referenziert `(tenant_id, id)` auf `contacts`
2. Eindeutiges `(tenant_id, id)` auf `contacts`
3. Dasselbe für alle mandantenbezogenen FK-Beziehungen
**Aufwand:** 2-4 Stunden
---
## P2 — Architektonische Konsolidierung
### P2-1: Unified Contact Model normalisieren
**Problem:** Eine Tabelle enthält Unternehmen, Personen, 3 Adressarten, Bankdaten, Steuernummern, Rabatte, Projektinformationen, Warnungen, Tags, Custom Fields, Suchindex. Dubletten zu vorhandenen Modellen für Adressen, Bankkonten, Tags, Custom Fields.
**Weitere Probleme:**
- Rabatte als `Float` statt `Numeric`/`Decimal`
- Keine DB-Checks für Werte 0-100
- Keine eindeutigen Kontakt-/Buchhaltungscodes pro Mandant
- Keine klare Validierung welche Felder bei Person/Firma erlaubt sind
- `surfix` — dauerhaft übernommener Tippfehler
- `JSON` statt `JSONB`
- Suche fest auf Deutsch eingestellt
- Keine normalisierten Suchschlüssel für E-Mail und Telefonnummer
- CSV-Import ohne Dubletten-/Encoding-/Dezimal-/Rollback-Strategie
**Maßnahme:**
1. Adressen in separate Tabelle auslagern (bereits vorhanden — nutzen)
2. Bankdaten in separate Tabelle (bereits vorhanden — nutzen)
3. Tags als Relation (bereits vorhanden — nutzen)
4. Custom Fields als Relation (bereits vorhanden — nutzen)
5. Rabatte: `Numeric(5,2)` statt `Float`
6. DB-Check: `discount_* BETWEEN 0 AND 100`
7. Eindeutige `(tenant_id, code)` und `(tenant_id, accounting_code)`
8. `surfix``suffix` (Migration mit Rename)
9. `JSON``JSONB`
10. Suchkonfiguration pro Mandant konfigurierbar
11. Normalisierte Suchschlüssel (lowercase, trimmed) für E-Mail und Telefon
12. CSV-Import: Dubletten-Erkennung, Encoding-Detection, Decimal-Parsing, Transaction-Rollback
**Aufwand:** 2-3 Tage
---
### P2-2: Plugin-Cross-Imports reduzieren
**Problem:** 204 direkte `from app.plugins.builtins` Imports zwischen Plugins. Automatisierung importiert Modelle/Services von Kommunikation, Mail, Kalender. Verteilter Monolith ohne Modulgrenzen.
**Maßnahme:**
1. Öffentliche Schnittstellen (Contracts) für jedes Modul definieren
2. Direkte Imports fremder Plugin-Modelle verbieten
3. Kommunikation nur über Events oder öffentliche Service-API
4. CI-Check: keine direkten Cross-Plugin-Imports
**Aufwand:** 1-2 Wochen
---
### P2-3: Commands und Statusmaschinen
**Problem:** Geschäftsoperationen als `Route → Service → mehrere flush/commit` statt als zentrale Commands. Statusstrings frei beschreibbar statt Statusmaschinen.
**Maßnahme:**
1. `Route → Command → Authorization → Domain Operation → Transaction → Audit → Outbox Events → Commit`
2. Explizite Statusmaschinen für Angebote, Aufträge, Rechnungen
3. Übergänge validiert und auditiert
**Aufwand:** 1-2 Wochen
---
### P2-4: SPA Path-Traversal-Schutz vervollständigen
**Problem:** `app/main.py` SPA-Catch-All blockiert `..` nur in bestimmten Positionen. `..` in anderen Positionen wird nicht erfasst.
**Datei:** `app/main.py` (spa_spa Funktion)
**Maßnahme:**
1. `os.path.abspath` gegen `frontend_dist` prüfen nach Join
2. Kein `..` in irgendeiner Position erlauben
**Aufwand:** 30 Minuten
---
## Zusammenfassung
| Priorität | Anzahl | Geschätzter Aufwand |
|---|---|---|
| P0 (sofort) | 7 | ~5-7 Tage |
| P1 (vor Kundendaten) | 11 | ~7-10 Tage |
| P2 (architektonisch) | 4 | ~2-4 Wochen |
| **Total** | **22** | **~4-6 Wochen** |
## Reihenfolge
### Woche 1: P0 absichern
1. P0-7: App von öffentlicher Domain nehmen (30 Min)
2. P0-1: Auth-Bypass entfernen (2-4h)
3. P0-3: Plugin-Upload deaktivieren (30 Min Sofort, langfristig später)
4. P0-6: Persistent Volume in Coolify (1-2h)
5. P0-2: Migrationen ersetzen (4-8h)
6. P0-4: RLS reparieren (1-2 Tage)
7. P0-5: Plugin-Doppelregistrierung beheben (1 Tag)
### Woche 2-3: P1 Fundament
8. P1-9: Metrics absichern (30 Min)
9. P1-8: Password Reset (2-4h)
10. P1-10: Coolify-Doku & Config korrigieren (2-3h)
11. P1-2: Redis zentralisieren (2-4h)
12. P1-5: XSS schließen (4-6h)
13. P1-11: Cross-Tenant FK (2-4h)
14. P1-1: User/Tenant-Modell (1 Tag)
15. P1-7: Permission-System (1-2 Tage)
16. P1-6: DMS lastfest (1-2 Tage)
17. P1-3: Worker auslagern (1-2 Tage)
18. P1-4: Transactional Outbox (2-3 Tage)
### Woche 4-6: P2 Architektur
19. P2-4: SPA Path-Traversal (30 Min)
20. P2-1: Contact Model normalisieren (2-3 Tage)
21. P2-2: Cross-Imports reduzieren (1-2 Wochen)
22. P2-3: Commands & Statusmaschinen (1-2 Wochen)
---
## Validierung nach jedem Fix
- [ ] Python-Syntax-Check: `python -m py_compile app/**/*.py`
- [ ] pytest: `pytest tests/ -x`
- [ ] Frontend-Typecheck: `cd frontend && npx tsc --noEmit`
- [ ] Frontend-Build: `cd frontend && npx vite build`
- [ ] Manueller Smoke-Test: Login, Kontakt erstellen, DMS-Upload
- [ ] Cross-Tenant-Test: Datensatz aus Mandant A kann nicht aus Mandant B gelesen werden
- [ ] Deployment: Coolify Deploy + Healthcheck prüfen