fix(PROJ-64): Session-Invalidation bei Passwort-Change + Datei-Permissions gehärtet

Security-Audit deckte zwei Medium-Findings auf: JWTs blieben bis zu 8h nach
Passwort-Change/-Reset oder Admin-TOTP-Reset gültig (kein Session-Invalidation),
und archivierte Mails/Anhänge wurden mit 0644/0755 statt 0600/0700 geschrieben.

- users.tokens_valid_after (neue Spalte) wird bei SetPassword() und
  InvalidateTokensBefore() gesetzt; ValidateToken() lehnt JWTs mit iat davor ab.
- Admin-TOTP-Reset revoked jetzt aktive Sessions des Zielnutzers.
- Mail-/Attachment-Dateien und ihre Verzeichnisse nur noch für den
  archivmail-Service-Account lesbar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-03 22:23:27 +02:00
co-authored by Claude Sonnet 5
parent 0ccbd5bafb
commit b286352d07
7 changed files with 96 additions and 9 deletions
+26 -1
View File
@@ -129,6 +129,14 @@ func (s *Store) initSchema(ctx context.Context) error {
_, err = s.pool.Exec(ctx, `
ALTER TABLE users ADD COLUMN IF NOT EXISTS list_page_size INT NOT NULL DEFAULT 25;
`)
if err != nil {
return err
}
// PROJ-64: tokens_valid_after invalidiert alle vor diesem Zeitpunkt ausgestellten JWTs
// (Passwort-Change/Reset, Admin-TOTP-Reset) — schließt Session-Hijack-Fenster.
_, err = s.pool.Exec(ctx, `
ALTER TABLE users ADD COLUMN IF NOT EXISTS tokens_valid_after TIMESTAMPTZ;
`)
return err
}
@@ -207,10 +215,27 @@ func (s *Store) SetPassword(ctx context.Context, id int64, newPassword string) e
if err != nil {
return fmt.Errorf("userstore: bcrypt: %w", err)
}
_, err = s.pool.Exec(ctx, `UPDATE users SET password_hash=$1 WHERE id=$2`, string(hash), id)
_, err = s.pool.Exec(ctx, `UPDATE users SET password_hash=$1, tokens_valid_after=NOW() WHERE id=$2`, string(hash), id)
return err
}
// InvalidateTokensBefore sets tokens_valid_after=NOW() so all JWTs issued before
// this call are rejected on next use (PROJ-64). Used e.g. after admin TOTP reset.
func (s *Store) InvalidateTokensBefore(ctx context.Context, id int64) error {
_, err := s.pool.Exec(ctx, `UPDATE users SET tokens_valid_after=NOW() WHERE id=$1`, id)
return err
}
// TokensValidAfter returns the tokens_valid_after timestamp for a user, or nil if unset.
func (s *Store) TokensValidAfter(ctx context.Context, id int64) (*time.Time, error) {
var t *time.Time
err := s.pool.QueryRow(ctx, `SELECT tokens_valid_after FROM users WHERE id=$1`, id).Scan(&t)
if err != nil {
return nil, err
}
return t, nil
}
// GetByID retrieves a user by their numeric ID.
func (s *Store) GetByID(id int64) (*User, error) {
ctx := context.Background()