security: remediate all P0-P2 audit findings (12 tasks)

P0 fixes:
- WebSocket origin checking (reject untrusted origins)
- WS session token moved from URL query param to first message frame
- WebAuthn login cookie now uses Secure flag via shared SetSessionCookie
- PostgreSQL sslmode configurable via POSTGRES_SSLMODE env (default: require)
- Fixed DatabaseDSN to use real password instead of masked placeholder

P1 fixes:
- Per-IP rate limiting middleware (auth: 5 req/s, invites: 2 req/s)
- Security headers on all responses (CSP, X-Frame-Options, nosniff, etc.)
- WS broadcasts scoped to server members (prevents cross-server data leak)
- Webhook tokens stored as SHA-256 hashes (not plaintext)

P2 fixes:
- CSRF protection via Origin header validation on state-changing requests
- MANAGE_CHANNELS permission enforced on channel update/delete
- Upload validation: 25MB limit, extension allowlist, server-side MIME check
- File serve: path traversal protection + Content-Disposition: attachment

Files: 23 changed, +499/-100. Builds clean (go build, go vet, npm build).
Zero CVEs (govulncheck, npm audit).
This commit is contained in:
2026-06-29 09:30:50 -04:00
parent 5373105368
commit 130187c7be
23 changed files with 504 additions and 105 deletions
+18 -3
View File
@@ -70,7 +70,7 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
return;
}
const socket = new WebSocket(`${getWsHost()}/ws?token=${encodeURIComponent(token)}`);
const socket = new WebSocket(`${getWsHost()}/ws`);
let reconnectDelay = 1000;
const maxReconnectDelay = 30000;
@@ -89,8 +89,9 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
};
socket.onopen = () => {
set({ connected: true });
reconnectDelay = 1000;
// Send auth token as the first message frame instead of a URL param.
// This keeps the token out of server logs, browser history, and Referer headers.
socket.send(JSON.stringify({ token }));
};
socket.onmessage = (event) => {
@@ -101,6 +102,20 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
return;
}
// Server sends {"type":"ready"} after successful auth.
if (data.type === 'ready') {
set({ connected: true });
reconnectDelay = 1000;
return;
}
if (data.type === 'error') {
// Auth failed or server sent an error. Close and don't reconnect.
console.error('ws error:', data);
socket.close();
return;
}
if (data.type === 'ping') {
get().send({ type: 'pong', payload: {} });
return;