Addresses 14 findings from the security audit across all severity levels:
CRITICAL:
- C-1: Add user blocking system (migration, DB queries, REST API, WS DM
send check) to prevent harassment via unconsented DMs
- C-2: Remove server version from unauthenticated /health and /info endpoints
to prevent fingerprinting
HIGH:
- H-1: Remove dangerous-settings feature from tauri-plugin-http
- H-3: Default allowSelfSigned to false in API client (was hardcoded true)
- H-4: Cap invite expiration to 30 days (720 hours)
- H-5: Add 256KB message size limit to LiveKit WS proxy (prevents OOM)
- H-6: Cap concurrent sessions to 25 per user (evicts oldest on overflow)
- H-8: Restrict /diagnostics/connectivity to ADMINISTRATOR role
MEDIUM:
- M-2: Deny access to legacy NULL-uploader unlinked attachments
- M-4: Log warnings on TOTP plaintext decryption fallback paths
- M-8: Remove acceptInvalidCerts from OG preview fetches
- M-10: Expand file upload blocklist (Java .class, OLE2, WASM, .lnk)
- M-12: Add LIMIT to ListInvites (200) and ListMembers (1000)
- M-14: Add CHECK constraint trigger on channels.type (text/voice/dm)
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
Security audit across all 11 sections (AUTH-001 through DATA-001) found
0 critical, 1 high, 7 medium, 15 low issues. This commit addresses:
- Add json:"-" to User.PasswordHash, User.TOTPSecret, Session.TokenHash
to prevent accidental serialization of sensitive fields (M7)
- Add X-Content-Type-Options: nosniff to file serve responses (M5)
- Apply owner-only guard to backup list endpoint for consistency (M6)
- Persist rate-limit lockouts to SQLite so they survive restarts (M2)
- Normalize DM non-participant responses to 404 to prevent oracle (L3)
- Add explicit per-entry expiry check in partial auth Lookup/Consume (L1)
- Truncate unknown WS message type to 64 chars before echo (L6)
- Rate-limit ping handler to 2/sec per user (L7)
- Replace raw error strings in update handlers with generic messages (L15)
- Update 4 tests to match new 404 behavior for DM non-participant
Private attachments were accessible without authentication if the UUID
was known. Added AuthMiddleware to the GET /api/v1/files/{id} route,
uploader_id tracking on uploads, and channel-level permission checks
(guild READ_MESSAGES, DM participant, admin bypass) in handleServeFile.
Migration 010 adds uploader_id column to attachments table.
8 new access-control tests covering all authorization paths.
Add migration 008 with dm_participants and dm_open_state tables.
Add dm_queries.go with GetOrCreateDMChannel, GetUserDMChannels,
OpenDM, CloseDM, IsDMParticipant, and GetDMRecipient functions.
Server:
- Extract image width/height on upload via image.DecodeConfig (header-only)
- Add width/height columns to attachments table (migration 007)
- Include dimensions in AttachmentInfo JSON (optional, backward-compatible)
Client:
- Reserve exact space for images using server-provided dimensions
- Fallback min-height: 200px for external/old images, cleared on load
- Remove premeasureAll() — was caching wrong heights for unloaded images
- Smart per-type height estimates: 32px dividers, 42/72px text, +220px
per image attachment, +320px for YouTube embeds
- Batch ResizeObserver corrections to single RAF with anchor-based scroll
preservation (topmost visible item stays in place)
- Fenwick tree for O(log n) offset lookups (replaces O(n) linear scans)
- CSS contain: layout style on .msg-image to isolate layout shift
Server:
- Fix YAML injection in LiveKit config generation (quote values)
- Revert token TTL to 4h (no server-side JWT revocation)
- Derive LiveKit publish permissions from user role (prevent SFU bypass)
- Add CAS guard for webhook/voice_leave race condition
- Add voice_leave broadcast to rollbackVoiceJoin (prevent ghost state)
- Limit webhook body to 64KB (prevent memory abuse)
- Add rate limit to voice_token_refresh handler (1/60s)
- Add LiveKit health check endpoint (GET /api/v1/livekit/health, 503 on degraded)
- Add voice_token_refresh WS handler for client-initiated token refresh
- Consolidate voice quality constants (single source of truth)
- Fix video limit TOCTOU race (count from DB instead of LiveKit API)
- Raise default voice_max_video from 10 to 25 (Discord parity)
- Add CountActiveCameras DB query
- Non-blocking broadcast send, circuit breaker, exponential backoff
- Close send channel before context cancel in serve.go
- Guard voice mute/deafen for active channel
- Delete orphaned message on attachment link failure
- Redact query string from proxy logs (prevent token leak)
- Use instance-level HTTP client for health checks (no redirect following)
- Set cmd.WaitDelay to prevent goroutine leak on Windows
- Log buildJSON marshal errors
Client:
- Refactor livekitSession.ts from singleton module to LiveKitSession class
- Share single AudioContext for all analysers (was 1 per participant)
- Extract createRoom() helper (DRY)
- Add token refresh timer (3.5h interval, re-arms on failure)
- Skip setSpeakers if unchanged (sort in-place, no allocations)
- Distinguish user-initiated leave from connection error in retry
- Add YouTube videoId validation (prevent iframe src injection)
- Add try/finally to disableCamera
- Wrap store subscription callbacks in try/catch
- Track and cancel initial scroll RAF on cleanup
- Add 5s timeout + encodeURIComponent to YouTube oEmbed fetch
- Clean raw mic stream on RNNoise suppressor failure
- Full voice cleanup on logout via cleanupAll()
Tests:
- Add 7 new server tests (webhook parsing, voice guards, quality fallback)
- Fix 2 pre-existing test failures (mute/deafen invalid payload)
Two fixes:
1. Member role (0x663) was missing USE_VIDEO (0x800) and SHARE_SCREEN (0x1000)
bits, causing "permission denied" when non-owner users tried to enable camera.
Migration 006 updates Member permissions to 0x1E63.
2. checkVideoMode() now checks voice.localCamera immediately instead of waiting
for the server's voice_state broadcast, so video grid shows instantly when
a single user enables their camera.
- Redesign ConnectPage with modern dark theme, profile cards with delete buttons, login/register toggle
- Add DPAPI-encrypted password saving with "Remember my password" checkbox
- Fix permission bit constants to match SCHEMA.md (Member role 0x663)
- Add migration 004 to fix existing Member role permissions
- Add comprehensive audit logging across all server packages (auth, admin, ws, setup)
- Add member_join WebSocket broadcast so new users appear in members list in real-time
- Add host URL normalization (strip scheme prefix) for reverse proxy compatibility
- Add REST API client, ChatService orchestrator, WebSocket service with reconnection
- Add model types (WsEnvelope payloads, API responses), converters, tests
- Scaffold Go module (github.com/owncord/server) with all package dirs
- config: koanf-based YAML loader with env var overrides, default generation
- db: pure-Go SQLite (modernc, no CGO), WAL mode, FK enforcement, full
15-table schema from SCHEMA.md including FTS5 and idempotent migrations
- auth/tls: ECDSA P-256 self-signed cert generation, LoadOrGenerate for
all 4 TLS modes (self_signed, acme, manual, off)
- api: chi router with request ID middleware, /health and /api/v1/info
- main: graceful shutdown (30s timeout), structured slog JSON logging
- Stubs for ws, storage, admin packages ready for Phase 2+
Test coverage: api 100%, auth 85.7%, db 82.4%, config 80.6%
Binary: chatserver.exe 12MB, GOOS=windows GOARCH=amd64