From c744eebfa375f24993d4b0434d2f12b473a38bc5 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 17 May 2026 01:26:12 +0200 Subject: [PATCH] Rate-Limit-Liste: bereits freigegebene IPs ausblenden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Liste basiert auf unveränderlichen SecurityEvents – ein Reset leerte nur den In-Memory-Limiter, aber die historischen Events blieben weitere 15 Min in der Anzeige stehen ("Freigeben klappt nicht"). Fix: für jede candidate-IP wird der letzte AuditLog-Eintrag (resourceType=RateLimit) im 15-Min-Fenster geprüft. Liegt er nach dem letzten Hit der IP, fliegt die IP aus der Liste – aber sobald wieder ein RATE_LIMIT_HIT nach dem Reset kommt, taucht die IP wieder auf. Live-verifiziert: trigger → 1 Eintrag; reset → 0 Einträge; erneuter trigger → 1 Eintrag. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/rateLimitAdmin.controller.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/backend/src/controllers/rateLimitAdmin.controller.ts b/backend/src/controllers/rateLimitAdmin.controller.ts index 573235bb..472600f2 100644 --- a/backend/src/controllers/rateLimitAdmin.controller.ts +++ b/backend/src/controllers/rateLimitAdmin.controller.ts @@ -61,6 +61,37 @@ export async function getActiveRateLimits(req: AuthRequest, res: Response): Prom } } + // Bereits manuell freigegebene IPs aus der Anzeige rauswerfen: wenn der + // letzte Reset (= Audit-Log-Eintrag) NACH dem letzten Hit liegt, ist die + // IP nicht mehr gesperrt. SecurityEvents sind unveränderlich, also brauchen + // wir diesen Reset-Marker, sonst bleibt eine bereits freigegebene IP + // weiterhin im Bildschirm hängen, bis das 15-Min-Fenster abgelaufen ist. + const candidateIps = Array.from(byIp.keys()); + if (candidateIps.length > 0) { + const recentResets = await prisma.auditLog.findMany({ + where: { + resourceType: 'RateLimit', + resourceId: { in: candidateIps }, + createdAt: { gte: since }, + }, + select: { resourceId: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }); + const resetMap = new Map(); + for (const r of recentResets) { + if (r.resourceId && !resetMap.has(r.resourceId)) { + resetMap.set(r.resourceId, r.createdAt); + } + } + for (const ip of candidateIps) { + const reset = resetMap.get(ip); + const entry = byIp.get(ip)!; + if (reset && reset >= entry.lastHit) { + byIp.delete(ip); + } + } + } + const list = Array.from(byIp.values()).sort( (a, b) => b.lastHit.getTime() - a.lastHit.getTime(), );