# PROJ-64: Session-Invalidation bei Passwort-Change + Datei-Permissions-Härtung (Security-Audit-Nachbesserung) ## Status: Deployed **Created:** 2026-07-03 **Last Updated:** 2026-07-03 ## Hintergrund Vollständiges Codebase-Security-Audit (2026-07-03, 4 parallele Fokus-Reviews: Auth/Authz, Injection, Tenant-Isolation, Crypto/Storage) ergab zwei Medium-/Medium-High-Findings. Injection- und Tenant-Isolation-Review lieferten keine ausnutzbaren Befunde (bestehende Fixes aus PROJ-55/61/62/63 halten). **Finding 1 — Kein Session-Invalidation bei Passwort-Change/Reset (Medium-High):** `SetPassword()` (Passwort-Change, Passwort-Reset) und Admin-TOTP-Reset aktualisierten nur den bcrypt-Hash bzw. TOTP-Status, invalidierten aber keine bereits ausgestellten JWTs. Die Token-Blacklist arbeitet nur per-JTI (Logout), es gab keinen "invalidate all sessions"-Mechanismus. Ein gestohlenes JWT/Cookie blieb bis zu 8h nach Passwort-Reset gültig — die Standard-Gegenmaßnahme "Passwort ändern, um Angreifer auszusperren" griff nicht. **Finding 2 — Mail-Dateien world-readable (Medium):** `internal/storage/storage.go` und `attachments.go` schrieben archivierte Mails/Anhänge mit Mode `0644` (Verzeichnisse `0755`) — lesbar für jeden lokalen User/Group, nicht nur den archivmail-Service-Account. Falls Encryption nicht konfiguriert ist (dokumentierter Fallback), lag Mail-Inhalt im Klartext für jeden lokalen Account offen. ## Acceptance Criteria - [x] Neue Spalte `users.tokens_valid_after` (TIMESTAMPTZ, nullable), idempotent per `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in `initSchema`. - [x] `SetPassword()` setzt `tokens_valid_after = NOW()` bei jedem Passwort-Change/-Reset. - [x] Neue Methode `InvalidateTokensBefore(ctx, userID)` — von Admin-TOTP-Reset aufgerufen. - [x] `ValidateToken()` vergleicht JWT-Claim `iat` gegen `tokens_valid_after`; liegt `iat` davor, wird der Token als revoked abgelehnt (`auth: token revoked (credentials changed)`). - [x] Bestehendes Verhalten unverändert für User ohne gesetztes `tokens_valid_after` (NULL-Check, No-op). - [x] Mail-Dateien: `os.WriteFile` von `0o644` auf `0o600`. - [x] Attachment-Dateien: gleiche Änderung in `attachments.go`. - [x] Storage-Verzeichnisse (`store/`, `attachments/`, `meta/`, Shard-Verzeichnisse): `os.MkdirAll` von `0o755` auf `0o700`. - [x] Build- und Schema-Verifikation auf Testserver 192.168.1.132 (kein lokaler Go-Toolchain verfügbar). ## Implementierungsnotizen (2026-07-03) - `internal/userstore/userstore.go`: Spalte + `SetPassword()` erweitert + `InvalidateTokensBefore()` + `TokensValidAfter()` ergänzt. - `internal/auth/auth.go` (`ValidateToken`): `iat`-Vergleich gegen `tokens_valid_after` ergänzt, zusätzlicher DB-Roundtrip pro Request-Validierung (gleiche Größenordnung wie bestehender Blacklist-Check). - `internal/api/totp_handlers.go` (`handleTOTPReset`): Aufruf von `InvalidateTokensBefore()` nach erfolgreichem TOTP-Reset ergänzt (Fehler nur geloggt, kein Abbruch — Reset selbst war bereits erfolgreich). - `internal/api/onboarding_handlers.go` (Passwort-Reset) und `internal/api/profile_handlers.go` (Passwort-Change) benötigten keine Änderung — beide rufen `SetPassword()` auf, das die Invalidation jetzt intern miterledigt. - `internal/storage/storage.go`: Zeilen 72 (Init-MkdirAll), 413 (Shard-MkdirAll), 446 (WriteFile) gehärtet. - `internal/storage/attachments.go`: Zeilen 45 (MkdirAll), 49 (WriteFile) gehärtet. - Build (`CGO_ENABLED=0 go build -buildvcs=false`) und Schema-Migration (`tokens_valid_after`-Spalte, klaglos angelegt) auf 192.168.1.132 verifiziert, Dienste liefen danach unverändert weiter. Kein Produktiv-Deploy im Rahmen dieser Verifikation. ## Tech Design Übersprungen (additive Härtung, kein neues Subsystem — Muster analog zu bestehender Blacklist-/TOTP-Reset-Logik). ## Priorität Medium — kein akuter Exploit im laufenden Betrieb, aber schließt eine reale Lücke in der Incident-Response ("Passwort ändern" wirkte bisher nicht sofort) und eine konkrete Klartext-Offenlegung bei fehlender Encryption-Konfiguration.