Passwörter, Sitzungen und Passwort-Reset in einem Kundenportal
Ein Portal speicherte Passwörter als SHA-256 ohne Salt, Anmeldungen waren nicht widerrufbar. Argon2id ohne Massenreset und Sitzungen, die sich beenden lassen.
- Firma
- Steuerberatungsbüro, 11 Beschäftigte, rund 150 Mandanten
- Anwendung
- Ein Portal, in das Mandanten Belege hochladen und Auswertungen herunterladen
- Team
- Gebaut von einem Freelancer, der nicht mehr verfügbar ist; ein neuer Entwickler in Teilzeit
- Technik
- Node.js, PostgreSQL, React, ein VPS
- Rahmen
- Ein Massenreset der Passwörter würde das Büro mit Anrufen überfluten
01Ausgangslage
Passwörter lagen in der Tabelle users als ungesalzenes hexadezimales SHA-256. Nach der Anmeldung stellte der Server ein JWT mit 30 Tagen Gültigkeit aus, das das Frontend in localStorage ablegte und im Header Authorization mitschickte. Die Abmeldung löschte das Token nur im Browser.
Der Link zum Zurücksetzen des Passworts enthielt ein zufälliges Token, das in der Datenbank lesbar und ohne Ablaufzeit gespeichert war.
- Browser (React, JWT in localStorage)
- Nginx
- Node.js-API
- PostgreSQL (Benutzer, Belege)
02Das Problem
Das Portal sollte eine elektronische Signatur bekommen. Beim Lesen des Anmeldecodes fielen dem neuen Entwickler drei zusammenhängende Schwächen auf, mit dem Vorschlag, sie zu beheben, bevor eine weitere sensible Funktion dazukommt.
Passwörter: SHA-256 ist eine schnelle Hashfunktion für Integritätsprüfungen, nicht für Passwörter. Wer eine Kopie der Tabelle bekommt, aus einem Backup oder über einen anderen Fehler, kann auf einer gewöhnlichen Grafikkarte Milliarden Kandidaten pro Sekunde durchprobieren. Ohne Salt haben gleiche Passwörter gleiche Hashes und lassen sich in vorberechneten Tabellen nachschlagen.
Sitzungen: Das Token ließ sich nicht widerrufen. Abmelden, Passwortwechsel oder Sperren eines Mandanten beendeten es nicht; es galt die vollen 30 Tage. In localStorage ist es für jedes JavaScript der Seite lesbar, ein XSS-Fehler würde also erlauben, es zu stehlen und von einem anderen Rechner aus zu nutzen.
Passwort-Reset: Wer die Datenbank oder eine alte E-Mail mit dem Link sah, konnte noch Monate später das Passwort eines fremden Kontos setzen.
Von einem Missbrauch weiß die Firma nichts. Es sind Schwächen, die die Auswirkung eines anderen Fehlers vergrößern: eines Datenbanklecks, eines XSS oder einer gestohlenen E-Mail.
03Technische Ursache
Der ursprüngliche Autor verwechselte Hashing zur Integritätsprüfung mit der Speicherung von Passwörtern. Für Passwörter gibt es absichtlich langsame Funktionen mit eigenem Salt pro Passwort und einstellbarem Aufwand: Argon2id, scrypt oder bcrypt. Hashing ist auch keine Verschlüsselung: Ein Hash lässt sich nicht entschlüsseln, nur erraten, und eine schnelle Funktion macht das Raten billig.
Ein JWT ist signiert, der Client kann es also nicht ändern, aber der Server führt nach der Ausstellung keinen Eintrag darüber. Ohne Sperrliste kann er es vor Ablauf nicht ungültig machen. Für die Anmeldung an einer einzelnen Webanwendung ist das Aufwand ohne Nutzen.
Das Reset-Token wurde als gewöhnlicher Wert behandelt, obwohl es ein temporäres Passwort ist.
04Untersuchung
Ein Blick in die Datenbank: wie viele Konten denselben Hash teilen. Mehrere Gruppen mit gleichem Hash bedeuteten gleiche, vermutlich schwache Passwörter.
SELECT password_hash, count(*) AS accounts
FROM users
GROUP BY password_hash
HAVING count(*) > 1
ORDER BY accounts DESC;Eine Bestandsaufnahme der Kopien der Tabelle users: nächtliche Backups auf einer externen Platte im Büro und eine Datenbankkopie auf dem Laptop des Freelancers aus der Entwicklungszeit. Beide sind so sensibel wie die Produktion.
Eine Durchsicht des Frontends nach Stellen, an denen HTML ohne Escaping eingefügt wird (dangerouslySetInnerHTML). Es gab eine, bei den Notizen zu Belegen.
05Behebung
Passwörter ohne Massenreset: Beim Deployment wurde jeder vorhandene SHA-256-Hash in Argon2id eingepackt, in der Datenbank steht also argon2id(sha256(passwort)). Die alte Form verschwand sofort für alle, ohne dass jemand ein Passwort eingeben musste. Bei der nächsten Anmeldung wird das Passwort direkt als argon2id(passwort) neu gehasht.
import argon2 from "argon2";
import { createHash } from "node:crypto";
import { db } from "./db.js";
// OWASP Password Storage Cheat Sheet: m = 19 MiB, t = 2, p = 1
const OPTS = { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 };
const sha256hex = (s) => createHash("sha256").update(s, "utf8").digest("hex");
// one-off, at deploy: wrap every legacy SHA-256 hash, nobody resets anything
export async function wrapLegacyHashes() {
const { rows } = await db.query(
"SELECT id, password_hash FROM users WHERE scheme = 'sha256'",
);
for (const u of rows) {
await db.query(
"UPDATE users SET password_hash = $1, scheme = 'argon2id(sha256)' WHERE id = $2",
[await argon2.hash(u.password_hash, OPTS), u.id],
);
}
}
// at login: verify, and replace a wrapped hash with a direct one
export async function verifyAndUpgrade(user, password) {
const wrapped = user.scheme === "argon2id(sha256)";
const ok = await argon2.verify(
user.password_hash,
wrapped ? sha256hex(password) : password,
);
if (ok && wrapped) {
await db.query(
"UPDATE users SET password_hash = $1, scheme = 'argon2id' WHERE id = $2",
[await argon2.hash(password, OPTS), user.id],
);
}
return ok;
}Parameter nach der OWASP-Empfehlung: 19 MiB Speicher, 2 Iterationen, 1 Thread. Alte Kopien mit SHA-256-Hashes, also die Backups und der Laptop des Freelancers, wurden gelöscht oder ersetzt. Solange sie existieren, schützt das Einpacken diese Konten nicht, weil ein Angreifer mit ihnen Argon2id einfach umgeht.
Serverseitige Sitzungen statt JWT: Die Anmeldung legt in der Datenbank einen Sitzungseintrag mit einer zufälligen 256-Bit-Kennung an, und der Browser erhält nur ein Cookie mit dieser Kennung.
Set-Cookie: __Host-sid=<random 256-bit id>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800Jede Anmeldung erzeugt eine neue Sitzung, was Session Fixation verhindert. Abmelden löscht den Eintrag, Passwortwechsel und Reset löschen alle Sitzungen des Benutzers, eine Sitzung gilt höchstens 8 Stunden. Anfragen, die Daten ändern, prüfen zusätzlich den Header Origin, denn SameSite schützt nicht vor Anfragen von anderen Subdomains derselben Domain.
Passwort-Reset: Das Token besteht aus 32 zufälligen Bytes, gespeichert wird nur sein SHA-256, es gilt 30 Minuten und wird nach Gebrauch gelöscht. Ein schnelles SHA-256 genügt hier, weil sich 256 Bit Zufall nicht erraten lassen, anders als ein Passwort, das sich ein Mensch ausgedacht hat.
Anmeldung: Fehlversuche werden pro Konto und pro IP-Adresse mit wachsender Verzögerung begrenzt statt mit dauerhafter Sperre, die ein Angreifer nutzen könnte, um fremde Konten zu sperren. Notizen zu Belegen werden als Text dargestellt, nicht als HTML.
06Warum die Lösung wirkt
Argon2id ist absichtlich langsam und speicherintensiv. Jeder Rateversuch kostet den Angreifer etwa so viel wie eine Anmeldung, und der Speicherbedarf begrenzt, wie viele Versuche eine Grafikkarte parallel schafft. Das Salt, das Argon2 für jedes Passwort erzeugt, sorgt dafür, dass gleiche Passwörter verschiedene Hashes haben und vorberechnete Tabellen nicht funktionieren.
Ein eingepackter Hash ist genauso teuer zu erraten: Ein Angreifer mit der neuen Tabelle muss bei jedem Versuch das volle Argon2id durchlaufen, das SHA-256 darin hilft ihm nur, wenn er auch eine alte Kopie hat.
Eine serverseitige Sitzung muss nur gelöscht werden, und der Zugang endet sofort. Ein HttpOnly-Cookie kann JavaScript nicht lesen, XSS kann es also nicht auf einen anderen Rechner mitnehmen. Ein __Host--Cookie akzeptiert der Browser nur mit Secure, Path=/ und ohne Domain, eine andere Subdomain kann es also nicht unterschieben.
In der Datenbank steht nur ein Hash des Reset-Tokens. Wer ihn sieht, gewinnt daraus das Token nicht.
Was das nicht löst: HttpOnly hindert XSS nicht daran, im offenen Browser des Benutzers in seinem Namen zu handeln. Ein langsamer Hash schützt ein schwaches Passwort nicht vollständig, er verlangsamt nur das Raten. Nichts davon schützt vor Phishing.
07Überprüfung
- Nach der Migration war keine Zeile mit dem Schema
sha256mehr übrig, und die Abfrage nach gleichen Hashes liefert nichts. - Automatische Tests: Anmeldung vor und nach dem Neuhashen, ein Passwortwechsel beendet die Sitzung in einem zweiten Browser, ein Reset-Link schlägt nach 30 Minuten und nach Gebrauch fehl.
- In den Entwicklerwerkzeugen des Browsers trägt das Cookie alle Attribute, und in
localStorageliegt nichts, was mit der Anmeldung zu tun hat. - Die Zahl der Konten mit eingepacktem Schema lässt sich jederzeit mit einer Abfrage ermitteln und sinkt mit jeder Anmeldung.
08Ergebnisse
Behoben
- In der Datenbank gibt es keine ungesalzenen SHA-256-Hashes.
- Abmelden und Passwortwechsel beenden den Zugang tatsächlich.
- Der Reset-Link ist einmalig und 30 Minuten gültig.
Risiko gesenkt
- Ein Leck der Datenbank oder eines Backups gäbe einem Angreifer einen deutlich langsameren Weg zu den Passwörtern.
- Ein XSS-Fehler kann die Anmeldung nicht mehr auf einen anderen Rechner stehlen.
Weiter offen
- Konten, die sich seitdem nicht angemeldet haben, behalten die eingepackte Form.
- Mandanten können weiterhin schwache Passwörter haben.
09Grenzen
- Ein schwaches Passwort fällt auch unter Argon2id, nur langsamer. Hilfreich ist die Prüfung neuer Passwörter gegen eine Liste geleakter Passwörter.
- Solange keine Zwei-Faktor-Authentifizierung aktiv ist, schützt nur das Passwort die Anmeldung, und Phishing funktioniert.
- Sitzungen in der Datenbank bedeuten eine zusätzliche Abfrage pro Anfrage. Bei der Größe dieses Portals ist das vernachlässigbar.
10Erkenntnisse
- Passwörter gehören in Argon2id, scrypt oder bcrypt, nie in SHA-256 oder MD5.
- Ein altes Hash-Schema lässt sich ohne Passwort-Reset ablösen: sofort einpacken, bei der Anmeldung neu hashen.
- Eine Anmeldung, die sich nicht widerrufen lässt, versagt genau dann, wenn sie beendet werden muss.
- Ein Reset-Token ist ein temporäres Passwort: nur den Hash speichern, kurz gültig, einmal verwendbar.
- Alte Datenbankkopien sind so sensibel wie die Produktion.
11Nächste Schritte für eine kleine Firma
- 1.Zwei-Faktor-Authentifizierung (TOTP), verpflichtend für das Büro, freiwillig für Mandanten.
- 2.Neue Passwörter gegen geleakte prüfen, etwa über die Pwned-Passwords-API, die dank k-Anonymität weder das Passwort noch seinen vollständigen Hash erhält.
- 3.Ein Content-Security-Policy-Header, der die Auswirkung künftiger XSS begrenzt.
- 4.Nach einem Jahr Konten mit noch eingepacktem Schema um ein neues Passwort bitten.
Erkennen Sie Ihre eigene Firma darin?
Schreiben Sie uns, worum es geht. Wir antworten innerhalb eines Werktags und sagen Ihnen, ob es Arbeit für uns ist, auch wenn die Antwort nein lautet.