# 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.
