Files
FlowReader/internal/handler/ratelimit.go
T
MichaelandClaude Opus 4.8 3402e53954 sécurité : correctifs XSS, SSRF, CSWSH, rate-limit et durcissement
- XSS stocké (critique) : sanitisation bluemonday sur tous les endpoints
  d'articles (List, ListByFeed, Favorites, Search, Get), pas seulement Get
- SSRF (élevé) : nouveau utils/safehttp.go (ValidateExternalURL + client durci
  via Dialer.Control, bloque IP privées/loopback/link-local/metadata,
  anti-DNS-rebinding et limite de redirections) appliqué à l'extracteur et au parser
- WebSocket CSWSH (élevé) : politique same-origin + override WS_ALLOWED_ORIGINS
- rate-limiting (moyen) : token-bucket en mémoire sur /auth/*
- token de session retiré du corps JSON (json:"-"), livré uniquement par le
  cookie HttpOnly
- cookie Secure correct derrière un reverse-proxy (X-Forwarded-Proto +
  override COOKIE_SECURE)
- admin : interdiction de supprimer son propre compte
- en-têtes de sécurité (nosniff, X-Frame-Options, Referrer-Policy,
  Permissions-Policy)

Note : backend non compilé localement (pas de toolchain Go) ; à valider via Docker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:21:43 +02:00

93 lines
2.2 KiB
Go

package handler
import (
"net/http"
"sync"
"time"
)
// rateLimiter is a simple in-memory token-bucket limiter keyed by client IP.
// It protects brute-force-prone endpoints (login/register) without external
// dependencies. Stale buckets are evicted periodically to bound memory.
type rateLimiter struct {
mu sync.Mutex
buckets map[string]*bucket
rate float64 // tokens added per second
capacity float64 // max tokens (burst)
}
type bucket struct {
tokens float64
last time.Time
}
// newRateLimiter allows `burst` requests immediately, refilling at
// `perMinute` requests per minute thereafter.
func newRateLimiter(perMinute, burst int) *rateLimiter {
rl := &rateLimiter{
buckets: make(map[string]*bucket),
rate: float64(perMinute) / 60.0,
capacity: float64(burst),
}
go rl.cleanupLoop()
return rl
}
func (rl *rateLimiter) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
b, ok := rl.buckets[key]
if !ok {
rl.buckets[key] = &bucket{tokens: rl.capacity - 1, last: now}
return true
}
// Refill based on elapsed time.
b.tokens += now.Sub(b.last).Seconds() * rl.rate
if b.tokens > rl.capacity {
b.tokens = rl.capacity
}
b.last = now
if b.tokens < 1 {
return false
}
b.tokens--
return true
}
func (rl *rateLimiter) cleanupLoop() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
for k, b := range rl.buckets {
// Drop buckets that have been idle long enough to be full again.
if time.Since(b.last) > 15*time.Minute {
delete(rl.buckets, k)
}
}
rl.mu.Unlock()
}
}
// Middleware returns a chi-compatible middleware enforcing the limit per IP.
func (rl *rateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !rl.allow(getClientIP(r)) {
w.Header().Set("Retry-After", "60")
respondError(w, http.StatusTooManyRequests, "Too many requests. Please slow down.")
return
}
next.ServeHTTP(w, r)
})
}
// NewAuthRateLimiter builds the limiter used for authentication routes:
// 10 requests/minute per IP with a small burst.
func NewAuthRateLimiter() func(http.Handler) http.Handler {
return newRateLimiter(10, 5).Middleware
}