130187c7be
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).
67 lines
1.6 KiB
Go
67 lines
1.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// ipLimiter tracks rate limiters per IP address.
|
|
type ipLimiter struct {
|
|
mu sync.Mutex
|
|
limiters map[string]*rate.Limiter
|
|
rate rate.Limit
|
|
burst int
|
|
}
|
|
|
|
func newIPLimiter(r rate.Limit, burst int) *ipLimiter {
|
|
return &ipLimiter{
|
|
limiters: make(map[string]*rate.Limiter),
|
|
rate: r,
|
|
burst: burst,
|
|
}
|
|
}
|
|
|
|
func (l *ipLimiter) getLimiter(ip string) *rate.Limiter {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
lim, exists := l.limiters[ip]
|
|
if !exists {
|
|
lim = rate.NewLimiter(l.rate, l.burst)
|
|
l.limiters[ip] = lim
|
|
}
|
|
return lim
|
|
}
|
|
|
|
// RateLimit returns middleware that limits requests per IP.
|
|
// requestsPerSecond is the sustained rate, burst is the max burst size.
|
|
func RateLimit(requestsPerSecond float64, burst int) func(http.Handler) http.Handler {
|
|
limiter := newIPLimiter(rate.Limit(requestsPerSecond), burst)
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ip := r.RemoteAddr
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
// Take the first IP in the chain (the original client)
|
|
if idx := strings.IndexByte(xff, ','); idx > 0 {
|
|
ip = strings.TrimSpace(xff[:idx])
|
|
} else {
|
|
ip = strings.TrimSpace(xff)
|
|
}
|
|
} else if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
|
ip = strings.TrimSpace(xri)
|
|
}
|
|
|
|
if !limiter.getLimiter(ip).Allow() {
|
|
http.Error(w, `{"error":"rate limited"}`, http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|