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>
29 lines
1.4 KiB
SQL
29 lines
1.4 KiB
SQL
-- Replay-Schutz fuer Refresh-Tokens (Pentest R164-02).
|
|
--
|
|
-- Die bisherige Rotation bot keinen Replay-Schutz: der alte Token blieb bis exp
|
|
-- gueltig (7 Tage), ein gestohlener Token war also parallel zum legitimen
|
|
-- nutzbar. Jetzt traegt jeder Refresh-Token eine jti und gehoert zu einer
|
|
-- Sitzungsfamilie; beim Einloesen wird die jti verbraucht. Taucht sie erneut
|
|
-- auf, wird die gesamte Familie widerrufen und der Vorfall gemeldet.
|
|
CREATE TABLE IF NOT EXISTS `RefreshTokenRecord` (
|
|
`id` INT NOT NULL AUTO_INCREMENT,
|
|
`jti` VARCHAR(191) NOT NULL,
|
|
`familyId` VARCHAR(191) NOT NULL,
|
|
`userId` INT NULL,
|
|
`customerId` INT NULL,
|
|
`isCustomerPortal` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`issuedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
`expiresAt` DATETIME(3) NOT NULL,
|
|
`usedAt` DATETIME(3) NULL,
|
|
`replacedByJti` VARCHAR(191) NULL,
|
|
`reuseCount` INT NOT NULL DEFAULT 0,
|
|
`revokedAt` DATETIME(3) NULL,
|
|
`revokedReason` VARCHAR(191) NULL,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `RefreshTokenRecord_jti_key` (`jti`),
|
|
KEY `RefreshTokenRecord_familyId_idx` (`familyId`),
|
|
KEY `RefreshTokenRecord_expiresAt_idx` (`expiresAt`),
|
|
KEY `RefreshTokenRecord_userId_idx` (`userId`),
|
|
KEY `RefreshTokenRecord_customerId_idx` (`customerId`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|