fix: Session-Pruefung akzeptierte Token anderer Zwecke
Alle Token in lib/session.ts sind mit DEMSELBEN Geheimnis signiert und
tragen denselben Aussteller und dieselbe Zielgruppe. Unterschieden werden
sie allein ueber ein "type"-Feld — und genau das prueft
verifySessionToken nicht. Es verlangte nur eine vorhandene "sub".
Damit bestand ein 2FA-Challenge-Token die Session-Pruefung und galt als
vollwertige Anmeldung. Dieses Cookie wird ausgestellt, NACHDEM das
Passwort stimmt, aber BEVOR der zweite Faktor geprueft ist: wer es im
Browser in das Session-Cookie kopiert, umgeht die Zwei-Faktor-Pruefung.
Praktisch nachgestellt:
signTwoFactorChallenge("customer_123")
-> verifySessionToken(...) ergab
{ customerId: "customer_123", role: "KUNDE", sessionVersion: 0 }
Dasselbe galt fuer den 2FA-Einrichtungstoken und den Token der
Backup-Code-Anzeige. Der Entra-OAuth-State trug keine "sub" und fiel
schon vorher durch.
Aufgehalten wurde das bisher nur zufaellig: diese Token fuehren keine
sessionVersion (also 0), und getCurrentCustomer vergleicht sie gegen die
Datenbank — wer 2FA aktiviert, steht dort bereits bei mindestens 1
(konto/actions/two-factor.ts zaehlt hoch). Das ist eine Sperre, die aus
einem voellig anderen Grund existiert und lautlos wegfaellt, sobald sich
an Zaehler oder Token etwas aendert.
verifySessionToken lehnt jetzt jeden Token mit "type"-Feld ab. Geprueft
wird auf ABWESENHEIT, nicht auf einen bestimmten Wert: das Session-Token
setzt selbst kein "type", bestehende Cookies bleiben also gueltig und
niemand wird durch das Ausrollen abgemeldet.
Dazu die erste Testabdeckung fuer die Token-Schicht ueberhaupt
(lib/session.test.ts, 12 Faelle): Rundlauf, manipulierter Token,
unbekannte Rolle wird auf KUNDE heruntergestuft, und jede Kombination von
Token-Verwechslung in beide Richtungen. Die Datei laeuft bewusst in der
node-Umgebung — jose prueft mit "instanceof Uint8Array", und jsdoms
TextEncoder liefert ein Uint8Array aus einem anderen Realm.
Nebenbei in honig/actions.ts: toggleWishlistAction nahm die productId
ungeprueft aus dem Formular. Ein erfundener Wert lief in eine
Fremdschluesselverletzung und damit in die allgemeine Fehlerseite — ein
500er als Antwort auf eine schlicht ungueltige Eingabe. Jetzt wird die
Existenz geprueft, und ein deaktiviertes Produkt kommt nicht neu auf eine
Wunschliste.
tsc: 0 Fehler. vitest: 328/328 gruen (zuvor 316). eslint: 0 Warnungen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
5170e477bc
commit
3e81896194
3 files changed
+200
No files matched your search
@@ -62,6 +62,29 @@ export async function verifySessionToken(token: string): Promise<SessionPayload
|
||||
algorithms: ["HS256"],
|
||||
});
|
||||
if (!payload.sub) return null;
|
||||
|
||||
// Alle Sondertoken in dieser Datei — 2FA-Challenge, 2FA-Einrichtung,
|
||||
// Backup-Code-Anzeige, Entra-OAuth-State — sind mit DEMSELBEN Geheimnis
|
||||
// signiert und tragen denselben Aussteller und dieselbe Zielgruppe. Ihr
|
||||
// einziger Unterschied ist das "type"-Feld. Ohne diese Zeile besteht ein
|
||||
// solcher Token die Pruefung hier und gilt als vollwertige Anmeldung:
|
||||
// das 2FA-Challenge-Cookie wird ausgestellt, NACHDEM das Passwort stimmt,
|
||||
// aber BEVOR der zweite Faktor geprueft ist — wer es in das
|
||||
// Session-Cookie kopiert, umgeht damit die Zwei-Faktor-Pruefung.
|
||||
//
|
||||
// Praktisch scheiterte das bisher nur zufaellig daran, dass diese Token
|
||||
// keine sessionVersion tragen (also 0) und getCurrentCustomer die Version
|
||||
// gegen die Datenbank prueft — wer 2FA aktiviert, steht dort bereits bei
|
||||
// mindestens 1 (siehe konto/actions/two-factor.ts). Das ist eine Sperre,
|
||||
// die aus einem voellig anderen Grund existiert; sie faellt lautlos weg,
|
||||
// sobald sich an Zaehler oder Token etwas aendert.
|
||||
//
|
||||
// Das Session-Token selbst setzt kein "type" (siehe signSessionToken).
|
||||
// Deshalb wird hier auf ABWESENHEIT geprueft statt auf einen bestimmten
|
||||
// Wert: so bleiben alle bereits ausgestellten Cookies gueltig, niemand
|
||||
// wird durch das Ausrollen abgemeldet.
|
||||
if (payload.type !== undefined) return null;
|
||||
|
||||
const role: SessionRole = payload.role === "ADMIN" ? "ADMIN" : "KUNDE";
|
||||
const sessionVersion = typeof payload.sessionVersion === "number" ? payload.sessionVersion : 0;
|
||||
return { customerId: payload.sub, role, sessionVersion };
|
||||
|
||||
Reference in new issue
Block a user