Refresh-Token: Replay-Schutz mit Familien-Widerruf (Pentest R164-02)
Die Rotation war wirkungslos: Der alte Refresh-Token blieb bis exp gueltig, ein gestohlener Token also bis zu 7 Tage parallel zum legitimen nutzbar - der Pentester trug mit einem Token 90 Parallel-Requests. Umgesetzt nach OAuth-Sicherheits-BCP: Jeder Refresh-Token traegt eine jti und gehoert zu einer Sitzungsfamilie (neue Tabelle RefreshTokenRecord). Beim Einloesen wird die jti verbraucht; taucht sie erneut auf, wird die gesamte Familie widerrufen und der Vorfall als SUSPICIOUS/CRITICAL gemeldet. Der Token selbst wird nicht gespeichert - die Signatur authentifiziert ihn bereits, und ein DB-Leck soll keine nutzbaren Sitzungen preisgeben. Kulanzfenster fuer parallele Tabs: 15 s und hoechstens 3 Wiederverwendungen. Ohne Toleranz wuerde der zweite legitime Tab die Sitzung sprengen; die enge Grenze laesst einen Missbrauchs-Burst trotzdem auflaufen. Das Einloesen ist atomar (bedingtes UPDATE statt Lesen-dann-Schreiben) - derselbe Fehlertyp wie bei der Audit-Kette: im ersten Testlauf kamen 90 gleichzeitige Requests ausnahmslos durch, weil alle den Token als unbenutzt lasen. Verifiziert: 90 parallele Requests -> nur 4 erfolgreich (1 + Kulanz 3), 27 als Replay erkannt, alle Folge-Tokens tot; 2 parallele Tabs weiterhin erfolgreich; gestohlener Token spaeter erneut abgewiesen; Logout widerruft die Familie; ueber HTTP kommt SUSPICIOUS/CRITICAL an. tsc + vite build gruen. Deploy-Hinweis: Refresh-Tokens ohne jti (Bestand vor dem Deploy) werden fail-closed abgewiesen - alle angemeldeten Nutzer muessen sich einmalig neu anmelden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,38 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🔁 Refresh-Token: Replay-Schutz mit Familien-Widerruf (Pentest R164-02)** (2026-08-18)
|
||||
- Die Rotation war bisher wirkungslos: Der alte Refresh-Token blieb bis `exp`
|
||||
gueltig, ein gestohlener Token also **bis zu 7 Tage parallel** zum legitimen
|
||||
nutzbar (der Pentester trug mit EINEM Token 90 Parallel-Requests).
|
||||
- Umgesetzt nach OAuth-Sicherheits-BCP: Jeder Refresh-Token traegt eine `jti`
|
||||
und gehoert zu einer Sitzungs-`familyId` (neue Tabelle
|
||||
`RefreshTokenRecord`, Migration `20260818160000`). Beim Einloesen wird die
|
||||
`jti` verbraucht; taucht sie erneut auf, wird die **gesamte Familie**
|
||||
widerrufen – Angreifer und legitimer Nutzer fliegen raus, der Nutzer merkt
|
||||
es und der Vorfall wird als `SUSPICIOUS / CRITICAL` gemeldet.
|
||||
- Der Token selbst wird NICHT gespeichert (die Signatur authentifiziert ihn
|
||||
bereits); ein DB-Leck gibt damit keine nutzbaren Sitzungen preis.
|
||||
- **Kulanzfenster** fuer parallele Tabs: 15 s UND hoechstens 3
|
||||
Wiederverwendungen. Ohne Toleranz wuerde der zweite legitime Tab die
|
||||
Sitzung sprengen; die enge Grenze laesst einen Missbrauchs-Burst trotzdem
|
||||
auflaufen.
|
||||
- **Einloesen ist atomar** (bedingtes UPDATE statt Lesen-dann-Schreiben) –
|
||||
derselbe Fehlertyp wie bei der Audit-Kette: im ersten Testlauf kamen 90
|
||||
gleichzeitige Requests ausnahmslos durch, weil alle den Token als „noch
|
||||
unbenutzt“ lasen. Jetzt entscheidet die Datenbank, wer zuerst war.
|
||||
- Verifiziert: Rotation vergibt neue `jti` in derselben Familie; 90 parallele
|
||||
Requests → nur 4 erfolgreich (1 + Kulanz 3), 27 als Replay erkannt, Rest
|
||||
widerrufen, alle daraus entstandenen Tokens tot; 2 parallele Tabs weiterhin
|
||||
erfolgreich; gestohlener Token spaeter erneut → abgewiesen; Logout
|
||||
widerruft die Familie; ueber HTTP kommt `SUSPICIOUS/CRITICAL` an.
|
||||
Audit-Regression unveraendert (25/25, 0 Forks, alle V3). `tsc` +
|
||||
`vite build` gruen.
|
||||
- **Deploy-Hinweis:** Refresh-Tokens ohne `jti` (Bestand vor dem Deploy)
|
||||
werden bewusst **fail-closed** abgewiesen (`REFRESH_LEGACY`, als LOW
|
||||
gemeldet, kein Angriffsindiz). Alle angemeldeten Nutzer muessen sich nach
|
||||
dem Deploy **einmalig neu anmelden**.
|
||||
|
||||
- [x] **⚓ Externer Anker: Audit-Kette HMAC-signiert (Hash-Version 3)** (2026-08-18)
|
||||
- Schliesst den nach R166/R167 verbliebenen Grenzfall: Bis Version 2 war die
|
||||
Kette selbsttragend – wer die DB schreiben kann, konnte jede Zeile aendern
|
||||
|
||||
Reference in New Issue
Block a user