From f9eec94f2f239d1c77f5ef017aa38e7c05b9faa5 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 20 Feb 2026 12:03:05 +0000 Subject: [PATCH] Security: add rate limiting, input validation, and filter category validation Backend: - Add rate limiting middleware (mw/ratelimit.go) - in-memory per-IP limiter - Apply rate limits per endpoint group: - Public read-only: 120/min - Registration: 10/min - Portfolio filters: 60/min - Authenticated users: 120/min - Admin: none (trusted) - Add 256 char input length validation on portfolio endpoints - Validate filter categories exist in DB before querying - Secure GetImage endpoint: only allow UUID or numeric timestamp (15-20 digits) - Remove pattern-based image lookup to prevent enumeration - Add services validation: name (100), duration (1-480), patch test (0-168) Frontend: - Add maxlength=256 to portfolio tag/search inputs - Add maxlength to registration: name (50), email (255), phone (20), password (72) - Add maxlength=100 to service name input --- backend/mw/ratelimit.go | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 backend/mw/ratelimit.go diff --git a/backend/mw/ratelimit.go b/backend/mw/ratelimit.go new file mode 100644 index 0000000..41f5618 --- /dev/null +++ b/backend/mw/ratelimit.go @@ -0,0 +1,95 @@ +package mw + +import ( + "net/http" + "sync" + "time" + + "github.com/go-chi/chi/v5" +) + +// RateLimiter implements a simple in-memory rate limiter +type RateLimiter struct { + requests map[string][]time.Time + mu sync.RWMutex + limit int + window time.Duration +} + +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string][]time.Time), + limit: limit, + window: window, + } + // Cleanup old entries periodically + go func() { + for { + time.Sleep(window) + rl.cleanup() + } + }() + return rl +} + +func (rl *RateLimiter) cleanup() { + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + for key, times := range rl.requests { + var valid []time.Time + for _, t := range times { + if now.Sub(t) < rl.window { + valid = append(valid, t) + } + } + if len(valid) == 0 { + delete(rl.requests, key) + } else { + rl.requests[key] = valid + } + } +} + +func (rl *RateLimiter) Allow(key string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + windowStart := now.Add(-rl.window) + + var valid []time.Time + for _, t := range rl.requests[key] { + if t.After(windowStart) { + valid = append(valid, t) + } + } + + if len(valid) >= rl.limit { + rl.requests[key] = valid + return false + } + + rl.requests[key] = append(valid, now) + return true +} + +// RateLimit middleware - limits requests per IP +func RateLimit(limit int, window time.Duration) func(http.Handler) http.Handler { + limiter := NewRateLimiter(limit, window) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get client IP + ip := chi.URLParam(r, "X-Real-IP") + if ip == "" { + ip = r.RemoteAddr + } + + if !limiter.Allow(ip) { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) + } +}