Files
opencrm/backend/prisma/migrations/20260818160000_refresh_token_replay/migration.sql
T
duffyduckandClaude Opus 5 9fcab6f17e 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>
2026-08-19 19:02:53 +02:00

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;