From 8e46dbbfedbb837ca3d18cc4df46032c080f976a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 12 Aug 2026 14:54:12 +0200 Subject: [PATCH] Globaler /api-Rate-Limit-Backstop (Pentest R148) Es gab bislang keinen generellen Limiter - nur Login/Passwort-Reset/ Staff-ReAuth/Consent; authentifizierte Endpoints waren gegen Enumeration/ DoS ungedrosselt. Neuer apiBackstopRateLimiter auf alle /api-Requests, vor den Routern gemountet (ergaenzt die feineren Limiter, ersetzt sie nicht). Key = nur IPv6-/56-normalisierte IP - bewusst nicht IP+User, da der User- Claim hier nur unverifiziert lesbar waere (authenticate laeuft erst pro Route) und ein Angreifer sonst per Fake-userId beliebig Buckets erzeugen koennte. Limit per Env API_RATE_LIMIT_PER_MIN (Default 1200/min, Floor 60), /api/health ausgenommen. Kein SecurityEvent pro Block (Flood-Amplification). Deckt zugleich den offenen IPv6-Rate-Limit-Test ausserhalb der Auth-Pfade ab. Verifiziert: 60x200 dann 429, health bleibt 200. Co-Authored-By: Claude Opus 4.8 --- backend/src/index.ts | 6 +++++ backend/src/middleware/rateLimit.ts | 41 +++++++++++++++++++++++++++++ docs/todo.md | 17 ++++++++++++ 3 files changed, 64 insertions(+) diff --git a/backend/src/index.ts b/backend/src/index.ts index 95e2c0dc..31da5c83 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -30,6 +30,7 @@ if (!process.env.DATABASE_URL && process.env.DB_USER && process.env.DB_PASSWORD process.env.DATABASE_URL = `mysql://${u}:${p}@${h}:${port}/${process.env.DB_NAME}`; } +import { apiBackstopRateLimiter } from './middleware/rateLimit.js'; import authRoutes from './routes/auth.routes.js'; import customerRoutes from './routes/customer.routes.js'; import addressRoutes from './routes/address.routes.js'; @@ -346,6 +347,11 @@ app.use('/api', (req, res, next) => { next(); }); +// Globaler Backstop-Rate-Limiter für ALLE /api-Requests (Pentest R148). +// Großzügige Obergrenze pro IP – ergänzt die feineren Limiter (Login etc.), +// die als erste greifen. Siehe middleware/rateLimit.ts für die Begründung. +app.use('/api', apiBackstopRateLimiter); + // Öffentliche Routes (OHNE Authentifizierung) app.use('/api/public/consent', consentPublicRoutes); diff --git a/backend/src/middleware/rateLimit.ts b/backend/src/middleware/rateLimit.ts index a194142e..582702dc 100644 --- a/backend/src/middleware/rateLimit.ts +++ b/backend/src/middleware/rateLimit.ts @@ -132,6 +132,47 @@ export const staffPasswordReAuthLimiter = rateLimit({ * (Pentest 2026-05-20 INFO 28.4). 30 Requests pro 15 min pro IP reicht * für legitime Kunden weit aus. */ +/** + * Globaler Backstop-Limiter für ALLE /api-Requests (Pentest R148). + * + * Hintergrund: Es gab bislang KEINEN generellen /api-Limiter – nur die + * dedizierten oben (Login, Passwort-Reset, Staff-Re-Auth, Consent). Damit war + * jeder authentifizierte Endpoint gegen Enumeration/Scripted-Abuse/DoS + * ungedrosselt. Dieser Limiter ist eine großzügige Obergrenze, KEIN Ersatz für + * die feineren Limiter (die feuern früher und bleiben aktiv). + * + * Key = NUR die (IPv6-/56-normalisierte) IP. Bewusst NICHT (IP+User): den + * User-Claim könnten wir hier nur unverifiziert aus dem Token lesen (die volle + * `authenticate`-Prüfung inkl. DB läuft erst pro Route). Ein Angreifer könnte + * dann mit gefälschten userId-Claims beliebig frische Buckets erzeugen und den + * Backstop umgehen. Per-Account-Präzision liefern ohnehin die Login-Limiter. + * + * Limit per Env `API_RATE_LIMIT_PER_MIN` (Default 1200/min ≈ 20/s pro IP) – + * für legitime Nutzung (auch mehrere Nutzer hinter NAT) weit ausreichend, + * bremst aber Flooding massiv. Healthcheck (`/api/health`) ist ausgenommen. + * + * KEIN SecurityEvent pro geblocktem Request: Bei einem Flood würde das den + * Security-/Audit-Store selbst zumüllen (Amplification). Die 429 stehen im + * Access-Log; die feineren Limiter melden weiterhin an das Monitoring. + */ +const API_RATE_LIMIT_PER_MIN = Math.max( + parseInt(process.env.API_RATE_LIMIT_PER_MIN || '', 10) || 1200, + 60, +); + +export const apiBackstopRateLimiter = rateLimit({ + windowMs: 60 * 1000, + limit: API_RATE_LIMIT_PER_MIN, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { + success: false, + error: 'Zu viele Anfragen in kurzer Zeit. Bitte einen Moment warten.', + }, + keyGenerator: (req): string => ipKeyGenerator(req.ip || 'unknown'), + skip: (req) => req.path === '/health', +}); + export const publicConsentRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 30, diff --git a/docs/todo.md b/docs/todo.md index 2ab1f3f2..02da85de 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,23 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🛡️ Globaler API-Rate-Limit-Backstop + BLZ-Guard-Härtung (Pentest R148)** (2026-08-12) + - **Backstop:** Neuer genereller Limiter auf ALLE `/api`-Requests + (`apiBackstopRateLimiter`, in `index.ts` vor den Routern). Vorher gab es + KEINEN generellen Limiter – nur Login/Passwort-Reset/Staff-ReAuth/Consent; + authentifizierte Endpoints waren gegen Enumeration/DoS ungedrosselt. + Key = **nur IPv6-/56-normalisierte IP** (bewusst nicht IP+User: der + User-Claim wäre hier nur unverifiziert lesbar → Bypass per Fake-Token). + Limit per Env `API_RATE_LIMIT_PER_MIN` (Default **1200/min**, Floor 60), + `/api/health` ausgenommen. Kein SecurityEvent pro Block (sonst Flood- + Amplification). Verifiziert (60×200→429, health bleibt 200). + - **Deckt den offenen IPv6-Test mit ab:** außerhalb der Auth-Pfade greift jetzt + ebenfalls ein IPv6-normalisierter Limiter. + - **BLZ-Poisoning-Guard:** prüft jetzt JEDEN Dataset-Eintrag (nicht nur die + erste Zeile) – 8-stellige BLZ, Wert `[Name]` oder `[Name,BIC]`, + `next.remove` = BLZ-Liste, `next.valid` = gültiges Datum. Banken **ohne BIC** + (`[Name]`, z.B. BLZ 60050009) korrekt zugelassen → `lookupBlz` liefert `bic:''`. + - [x] **🔄 BLZ-/Bankdaten: Auto-Update via Volume + Einstellungen** (2026-08-12) - Neue Einstellungen-Seite **Einstellungen → Bankdaten (BLZ)** (`/settings/bank-data`): zeigt Datenstand (aktive Quelle Volume/Image, Version, Anzahl Banken,