From 1aa47da6a366b6d220c6aa97757a3dfb8197bc90 Mon Sep 17 00:00:00 2001 From: DmitrL-dev <84296377+DmitrL-dev@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:48:27 +1000 Subject: [PATCH] fix(quota): plan-aware scan limits + add quota stress test script --- internal/transport/http/soc_handlers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/transport/http/soc_handlers.go b/internal/transport/http/soc_handlers.go index 7365caa..836bc9f 100644 --- a/internal/transport/http/soc_handlers.go +++ b/internal/transport/http/soc_handlers.go @@ -1532,17 +1532,24 @@ func (s *Server) handlePublicScan(w http.ResponseWriter, r *http.Request) { return } - // Check usage quota (free tier: 1000 scans/month) + // Check usage quota — plan-aware (free=1000, starter=100k, pro=500k, enterprise=unlimited) if s.usageTracker != nil { userID := "" + planLimit := 1000 // default: anonymous/free if claims := auth.GetClaims(r.Context()); claims != nil { userID = claims.Sub + // Resolve tenant plan limit for authenticated users + if claims.TenantID != "" && s.tenantStore != nil { + if tenant, err := s.tenantStore.GetTenant(claims.TenantID); err == nil { + planLimit = tenant.ScanLimit() + } + } } ip := r.RemoteAddr // T4-3 FIX: Do NOT trust X-Forwarded-For here. // Trusting XFF allows attackers to rotate IPs and bypass quota entirely. // When behind a trusted proxy, configure it to set X-Real-IP. - remaining, err := s.usageTracker.RecordScan(userID, ip) + remaining, err := s.usageTracker.RecordScanWithLimit(userID, ip, planLimit) if err != nil { w.Header().Set("X-RateLimit-Remaining", "0") writeError(w, http.StatusTooManyRequests, "monthly scan quota exceeded — upgrade your plan at syntrex.pro/pricing")