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:
+18
-3
@@ -18,6 +18,7 @@ import (
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
|
||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
|
||||
@@ -58,6 +59,13 @@ func main() {
|
||||
// Session store for middleware
|
||||
sessionStore := auth.NewSessionStore(database.DB, cfg)
|
||||
|
||||
// WebSocket origin allowlist
|
||||
wsOrigins := []string{"https://" + cfg.Host}
|
||||
if cfg.Host == "localhost" {
|
||||
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
|
||||
}
|
||||
gateway.SetAllowedOrigins(wsOrigins)
|
||||
|
||||
// WebSocket hub
|
||||
hub := gateway.NewHub(database.DB, logger)
|
||||
go hub.Run()
|
||||
@@ -86,10 +94,14 @@ func main() {
|
||||
// Auth handler
|
||||
authHandler := auth.NewHandler(database.DB, cfg)
|
||||
|
||||
// Permissions checker
|
||||
permissionsChecker := permissions.NewChecker(database.DB)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
r.Use(chimw.RequestID)
|
||||
r.Use(middleware.SecurityHeaders)
|
||||
|
||||
// Health check
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -104,8 +116,9 @@ func main() {
|
||||
|
||||
// API routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Auth (public: register, login, logout)
|
||||
// Auth (public: register, login, logout) with strict rate limiting
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
|
||||
authHandler.RegisterPublicRoutes(r)
|
||||
})
|
||||
|
||||
@@ -113,6 +126,7 @@ func main() {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Session(sessionStore, cfg))
|
||||
r.Use(middleware.RequireAuth)
|
||||
r.Use(middleware.CSRFProtect([]string{"https://" + cfg.Host, "http://localhost:" + cfg.Port}))
|
||||
|
||||
// Auth (protected: me, update profile)
|
||||
authHandler.RegisterProtectedRoutes(r)
|
||||
@@ -123,7 +137,7 @@ func main() {
|
||||
|
||||
// Channels (nested under servers)
|
||||
r.Route("/{serverID}/channels", func(r chi.Router) {
|
||||
channel.NewHandler(database.DB).RegisterRoutes(r)
|
||||
channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -191,8 +205,9 @@ func main() {
|
||||
webhook.NewHandler(database.DB, hub).RegisterRoutes(r)
|
||||
})
|
||||
|
||||
// Invites
|
||||
// Invites (rate limited to prevent brute-force join)
|
||||
r.Route("/invites", func(r chi.Router) {
|
||||
r.Use(middleware.RateLimit(2, 5))
|
||||
invite.NewHandler(database.DB).RegisterRoutes(r)
|
||||
})
|
||||
})
|
||||
|
||||
+32
-8
@@ -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{}),
|
||||
|
||||
Reference in New Issue
Block a user