fix: resolve 24 critical and high issues from full code & security review

CRITICAL (5):
- Hub panic recovery now calls h.Stop() after 3 panics (ws/hub.go)
- Ring buffer EventsSince returns non-nil empty slice for current seq (ws/ringbuffer.go)
- PTT event listener stores unsubscribe handle to prevent leak (ptt.ts)
- verifyTotp respects config.allowSelfSigned instead of hardcoding (api.ts)
- ptt_listen_for_key uses spawn_blocking to avoid thread pool starvation (ptt.rs)

HIGH - Server (13):
- TOTP rate-limit checked after body decode; counters reset on success
- TOTP enable returns 409 if already enabled (must disable first)
- Global search pre-computes accessible channel IDs for FTS WHERE clause
- DeleteAccount queries roles by name instead of hard-coded IDs
- BackupToSafe uses absClean in VACUUM INTO
- Voice camera slot uses atomic EnableCameraIfUnderLimit DB method
- readPump snapshots voiceChID before unregister for TOCTOU safety
- Voice join sets state after token send; rollback takes broadcast flag
- Updater download uses probe pattern instead of overflow write
- Webhook checks Authorization header before reading body
- Storage.Save adds fsync and fixes double-close
- Default WS origin denies cross-origin (was: accept all)

HIGH - Client (6):
- WS reconnect uses generation counter to discard stale events
- AudioPipeline uses generation counter against stale worklet callbacks
- Screenshare mute state preserved across reconnect (not full leave)
- handleVoiceToken uses iterative loop instead of unbounded recursion
- store.ts re-entrancy guard with pending update queue
- Notification AudioContext cleaned up on logout

Reviewed by 4 parallel agents across Server Core, Server Realtime,
Client & Tauri, and Security. 55 total findings; 24 CRITICAL+HIGH
fixed here, 31 MEDIUM+LOW tracked in vault backlog (T-265–T-295).
This commit is contained in:
jevb
2026-04-01 09:23:17 +02:00
parent 30fd7fd880
commit a40b42bbed
28 changed files with 590 additions and 186 deletions
+10 -2
View File
@@ -124,7 +124,12 @@ func (s *Storage) Save(uuid string, r io.Reader) error {
if err != nil {
return fmt.Errorf("creating file %s: %w", dst, err)
}
defer f.Close() //nolint:errcheck
closed := false
defer func() {
if !closed {
_ = f.Close()
}
}()
// Reconstruct the full stream: header bytes we already read + remainder.
maxBytes := int64(s.maxSizeMB) * 1024 * 1024
@@ -138,14 +143,17 @@ func (s *Storage) Save(uuid string, r io.Reader) error {
if written == maxBytes {
var probe [1]byte
if n, _ := full.Read(probe[:]); n > 0 {
// File exceeds limit — remove the partial write and reject.
_ = f.Close()
closed = true
if removeErr := os.Remove(dst); removeErr != nil {
slog.Error("storage: failed to remove oversized file", "path", dst, "err", removeErr)
}
return fmt.Errorf("file exceeds maximum size of %d MB", s.maxSizeMB)
}
}
if syncErr := f.Sync(); syncErr != nil {
return fmt.Errorf("syncing file %s: %w", dst, syncErr)
}
return nil
}