Harden backend against critical and high security vulnerabilities
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m51s

- Fix WebSocket CheckOrigin to use proper url.Parse instead of string stripping
- Add admin auth checks to Users/User GraphQL queries
- Remove GraphQL GET transport to prevent CSRF via cross-site links
- Add application-level IP-based login rate limiting (5 attempts/min)
- Add path traversal bounds check on radio file upload
- Require DEV_MODE for GraphQL introspection and playground
- Move notes backend endpoint behind admin middleware
- Add dedicated Nginx rate limit zone for GraphQL (10r/s)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 13:27:19 +01:00
parent 798c8e7f50
commit b56f8253d9
8 changed files with 93 additions and 11 deletions

View File

@@ -0,0 +1,45 @@
package services
import (
"sync"
"time"
)
type RateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
max int
window time.Duration
}
func NewRateLimiter(max int, window time.Duration) *RateLimiter {
return &RateLimiter{
attempts: make(map[string][]time.Time),
max: max,
window: window,
}
}
func (rl *RateLimiter) Allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Remove expired entries
valid := rl.attempts[key][:0]
for _, t := range rl.attempts[key] {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) >= rl.max {
rl.attempts[key] = valid
return false
}
rl.attempts[key] = append(valid, now)
return true
}

View File

@@ -2,7 +2,7 @@ package services
import (
"net/http"
"strings"
"net/url"
"sync"
"time"
@@ -24,11 +24,12 @@ var Upgrader = websocket.Upgrader{
if origin == "" {
return false
}
origin = strings.TrimPrefix(origin, "https://")
origin = strings.TrimPrefix(origin, "http://")
// Strip port for localhost comparisons (e.g. "localhost:80")
host := strings.Split(origin, ":")[0]
return origin == allowedDomain || origin == "www."+allowedDomain || host == "localhost"
u, err := url.Parse(origin)
if err != nil {
return false
}
host := u.Hostname()
return host == allowedDomain || host == "www."+allowedDomain || host == "localhost"
},
}