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
@@ -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)
})
})