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
+32 -8
View File
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"strings"
@@ -26,7 +27,8 @@ type WSConn struct {
closed bool
}
// ConnectWS dials the websocket endpoint with the session cookie.
// ConnectWS dials the websocket endpoint and authenticates via an initial
// message frame (not a URL query parameter, to avoid leaking the token in logs).
func ConnectWS(serverURL, token string) (*WSConn, error) {
u, err := url.Parse(serverURL)
if err != nil {
@@ -39,18 +41,40 @@ func ConnectWS(serverURL, token string) (*WSConn, error) {
u.Scheme = "ws"
}
u.Path = "/ws"
q := u.Query()
q.Set("token", token)
u.RawQuery = q.Encode()
header := make(map[string][]string)
header["Cookie"] = []string{"dumpster_session=" + token}
conn, _, err := websocket.DefaultDialer.Dial(u.String(), header)
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return nil, err
}
// Send auth token as the first message frame.
authMsg, _ := json.Marshal(map[string]string{"token": token})
if err := conn.WriteMessage(websocket.TextMessage, authMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("send auth: %w", err)
}
// Wait for the server's ready response.
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
_, raw, err := conn.ReadMessage()
if err != nil {
conn.Close()
return nil, fmt.Errorf("auth timeout: %w", err)
}
var resp struct {
Type string `json:"type"`
Error string `json:"error"`
}
if err := json.Unmarshal(raw, &resp); err != nil || resp.Type != "ready" {
conn.Close()
msg := "unexpected response"
if resp.Error != "" {
msg = resp.Error
}
return nil, fmt.Errorf("auth failed: %s", msg)
}
conn.SetReadDeadline(time.Time{})
ws := &WSConn{
conn: conn,
done: make(chan struct{}),