← All posts

Unsere Sperre nach fünf falschen Passwörtern hat nie jemanden ausgesperrt

Authagonal·August 5, 2026

Kontosperren nach zu vielen Fehlversuchen sind Pflichtprogramm. Fünf falsche Passwörter, und das Konto ist für zehn Minuten gesperrt, was das Erraten von Passwörtern im Netz von einer echten Bedrohung zu einer Sackgasse macht: Ein Angreifer hat fünf Versuche pro Konto und pro zehn Minuten, also praktisch nichts. Wir hatten das. Der Schwellenwert war fünf, das Zeitfenster zehn Minuten, der Code, der das prüfte, war korrekt, und er funktionierte bei jedem Test einwandfrei, denn bei jedem Test tippten wir das falsche Passwort fünfmal hintereinander ein, eines nach dem anderen, wie ein Mensch.

Ein Angreifer ist kein Mensch, der eines nach dem anderen eintippt. Ein Angreifer schickt fünfhundert auf einmal. Und als wir genau das taten, bewegte sich der Zähler, der bei fünf hätte stoppen sollen, kaum, und das Konto wurde nie gesperrt. Man konnte einem einzelnen Konto beliebig viele Rateversuche entgegenwerfen, und die eine Schutzmaßnahme, die genau das verhindern sollte, saß tatenlos da, nicht weil die Logik falsch war, sondern weil sie annahm, immer nur eine Frage auf einmal gestellt zu bekommen.

Ein Zähler, der sich verzählt

Der Code für den fehlgeschlagenen Login tat das Naheliegende. Bei einem falschen Passwort lud er den Benutzer, erhöhte dessen Fehlversuchszähler um eins, prüfte, ob der neue Wert fünf erreicht hatte, und speicherte den Benutzer zurück. Lesen, ändern, schreiben. So aufgeschrieben, wie ein Mensch darüber nachdenkt, ist er korrekt.

So aufgeschrieben, wie zwei Anfragen im selben Moment eintreffen, ist er es nicht. Beide lesen den Zählerstand, und beide lesen denselben Wert, sagen wir drei. Beide addieren eins und erhalten vier. Beide schreiben vier zurück. Es fanden zwei Fehlversuche statt, und der Zähler ging von drei auf vier. Eines der Inkremente verschwand einfach, überschrieben vom anderen, weil beide vom selben veralteten Ausgangspunkt aus arbeiteten und der zweite Schreibvorgang sich über den ersten legte, als hätte es den ersten nie gegeben.

Feuere nun fünfhundert falsche Passwörter auf einmal ab. Sie stapeln sich genau auf diese Weise übereinander, jedes liest einen niedrigen Stand und schreibt einen etwas weniger niedrigen zurück, während Inkremente zu Hunderten verdampfen. Der Zähler treibt ein wenig nach oben und bleibt weit unter fünf stehen. Der Schwellenwert für die Sperre wird nie überschritten, nicht weil die Prüfung falsch ist, sie wird jedes einzelne Mal gewissenhaft ausgeführt, sondern weil die Zahl, die sie prüft, eine Lüge ist, zusammengesetzt von einer Menge von Anfragen, die sich alle gegenseitig überschreiben. Der Brute-Force-Schutz wird nicht durch einen Logikfehler ausgehebelt, sondern durch Nebenläufigkeit, und das ist die Art von Fehler, die jeden seriellen Test besteht, den du je schreiben wirst, und in dem Moment versagt, in dem sie auf einen echten Angreifer trifft, den einzigen, der ein Motiv hat, Anfragen parallel zu schicken.

Wo dieser Fehler saß, ist wichtig. Er war nicht in irgendeiner Speicherschicht vergraben. Er stand direkt im Login-Handler, offen sichtbar, ein ++ und ein Speichern, der natürlichste Code der Welt. Nichts daran deutet beim Lesen auf Gefahr hin. Man muss daran denken, dass zwei davon gleichzeitig laufen, damit der Fehler auftaucht, und Code Zeile für Zeile zu lesen ist genau die Geisteshaltung, in der man das nicht tut.

Einen Zähler atomar machen auf einem Speicher, der nicht zählen kann

Die Lösung muss das gesamte Lesen-Ändern-Schreiben unteilbar machen: Kein anderer Versuch darf sich zwischen unser Lesen und unser Schreiben drängen. Das Interessante ist, dass wir auf einem Key-Value-Store ohne Increment-Operation arbeiten. Man kann ihm nicht sagen "addiere eins zu diesem Feld", also das Primitiv, das einem eine Datenbank mit atomaren Zählern in die Hand geben würde. Man kann nur eine Zeile lesen und eine Zeile schreiben.

Was der Store einem sehr wohl bietet, ist ein bedingter Schreibvorgang. Jede Zeile trägt eine Versionsmarke, die sich bei jedem Schreiben ändert, und man kann sagen: Schreib diese Zeile zurück, aber nur, wenn ihre Version noch die ist, die ich gelesen habe. Falls jemand anderes zwischenzeitlich geschrieben hat, hat sich die Version verschoben, und dein Schreibvorgang wird abgelehnt, statt ihren stillschweigend zu zerstören.

Das verwandelt das Rennen um verlorene Inkremente in eine erkannte Kollision. Lies die Zeile und ihre Version. Addiere eins. Versuche, unter der Bedingung dieser Version zurückzuschreiben. Gelingt es, landete dein Inkrement mit Sicherheit auf dem Wert, den du gelesen hast, ohne dass etwas verloren ging. Wird es abgelehnt, bedeutet das, dass ein anderer Versuch dir zuvorgekommen ist, also liest du den nun höheren Stand erneut und versuchst es noch einmal. Wiederhole bei Ablehnung, eine begrenzte Handvoll Male, und jedes Inkrement wird gezwungen, sich hinter dem letzten einzureihen, statt es zu überschreiben. Der Zählerstand wird wieder wahr, und der Angriff mit fünfhundert auf einmal treibt den Zähler nun schnurstracks auf fünf und löst die Sperre aus, was der ganze Sinn der Sache ist.

Wir übertrugen dieselbe Form auf unser anderes Speicher-Backend, das ebenfalls kein Increment hat, aber ebenfalls einen bedingten Schreibvorgang bietet, diesmal an den Zählerwert selbst gebunden statt an eine Versionsmarke: Schreib den neuen Stand nur, wenn der gespeicherte Stand noch der ist, den ich gelesen habe. Dieselbe Garantie, ausgedrückt im Vokabular dieses Stores. Die Lehre gilt über jede einzelne Datenbank hinaus: Wenn dir ein atomares Increment fehlt, rekonstruiert ein bedingter Schreibvorgang plus eine Wiederholschleife eines, denn die Bedingung ist das, was das veraltete Überschreiben verweigert, das deinen Zählerstand verlor.

Eine bewusste Asymmetrie ist erwähnenswert. Wir machten den Fehler-Pfad atomar und ließen den Erfolgspfad, der den Zähler bei einem korrekten Login auf null zurücksetzt, als schlichtes Schreiben nach dem Prinzip letzter-gewinnt. Nebenläufigkeit bedroht ein Zurücksetzen nicht: Mehrere erfolgreiche Logins, die um die Wette laufen, wollen alle null schreiben, und es spielt keine Rolle, welcher gewinnt. Nur das Inkrement ist feindselig, weil nur das Inkrement das ist, was ein Angreifer niedrig zu halten versucht. Du gibst die Kosten für bedingte Schreibvorgänge und Wiederholungen dort aus, wo ein Gegner drückt, und nicht dort, wo er es nicht tut.

Geh davon aus, dass jemand mitzählt

Der eigentliche Wert dieses Fehlers war die Denkweise, zu der er uns zwang, denn am selben Tag, an dem wir ihn behoben, machten wir uns auf die Suche nach allem anderen, das stillschweigend von einer-Anfrage-auf-einmal ausging oder annahm, dass niemand mitmisst, und fanden ein kleines Nest von Geschwistern.

Unsere Login-Antworten verrieten Angreifern, welche Konten existierten. Ein fehlendes Konto, ein deaktiviertes, ein gesperrtes und ein falsches Passwort erzeugten jeweils eine unterscheidbare Antwort, und der Pfad für den unbekannten Benutzer kam sogar schneller zurück, weil er das Passwort-Hashing komplett übersprang, sodass ein Angreifer entweder am Status oder an der Uhr ablesen konnte, wer ein Konto hat. Wir zogen die Antworten zusammen: Ein unbekannter Benutzer und ein falsches Passwort liefern nun die identische Antwort, und ein unbekannter Benutzer wird weiterhin gegen einen festen Dummy-Hash geprüft, damit das Timing dasselbe ist, ob das Konto existiert oder nicht. Das Konto existiert oder nicht, und von außen kann man es nicht erkennen.

Die Registrierung hatte dasselbe Leck in schwierigerer Form, denn man kann nicht einfach immer Erfolg melden, wenn jemand versucht, eine bereits vergebene E-Mail-Adresse zu registrieren, da man ihm damit scheinbar das Konto überreichen würde. Also liefert eine Registrierung für eine bereits registrierte Adresse nun genau denselben neutralen Erfolg wie eine frische Anmeldung, verbraucht dieselbe Zeit fürs Passwort-Hashing, legt nichts an und schickt stattdessen dem echten Inhaber eine E-Mail mit dem Hinweis, dass jemand versucht hat, seine Adresse zu registrieren. Der Angreifer erfährt nichts. Der tatsächliche Inhaber wird stillschweigend informiert.

Und das Formular zum Zurücksetzen des Passworts war eine E-Mail-Kanone: kein Limit pro Adresse, sodass man uns dazu bringen konnte, wieder und wieder Reset-Mails an eine beliebige Adresse abzufeuern, ein Angriff, der auf einen Dritten und auf unsere eigene Versandreputation zielt. Es begrenzt nun die Rate pro Zieladresse und überspringt oberhalb des Limits den Versand, während es dieselbe Erfolgsantwort wie immer zurückgibt, damit die Drosselung selbst nicht zu einer neuen Möglichkeit wird, zu erkunden, welche Adressen existieren.

Jedes davon ist die Lehre des Sperr-Fehlers in anderem Gewand. Entwirf so, als schicke ein Angreifer Anfragen parallel, stoppe deine Antwortzeiten und zähle mit. Denn das Einzige, dessen du dir sicher sein kannst, ist, dass die Person, die deinen Login angreift, es nicht eine höfliche Anfrage nach der anderen tut.

Warum ein Sicherheitszähler nebenläufig getestet werden muss

Ein Nebenläufigkeitsfehler in einer Sicherheitskontrolle ist schlimmer als ein Nebenläufigkeitsfehler an jeder anderen Stelle, aus einem bestimmten Grund: Die Partei mit dem stärksten Motiv, ihn auszulösen, ist der Angreifer, und die Bedingungen, die ihn auslösen, viele gleichzeitige Anfragen, sind genau die Bedingungen, die ein Angriff schafft und ein Test fast nie. Also lässt sich eine Sperre, eine Ratenbegrenzung, ein Kontingent, jede auf einem Zähler aufgebaute Kontrolle, nicht dadurch validieren, dass man ihn in einer Schleife hochzählt und zusieht, wie er auslöst. Er muss nebenläufig getroffen werden, denn Nebenläufigkeit ist die natürliche Umgebung des Angreifers, und die serielle Variante des Tests beweist nur, dass deine Kontrolle gegen einen Gegner funktioniert, der sich bereit erklärt hat, höflich zu sein.

Und wenn du den Zähler härtest, ist das Werkzeug auf einem Speicher ohne atomare Increments der bedingte Schreibvorgang: Lies die Version, schreib nur, wenn sie sich nicht bewegt hat, wiederhole, falls doch. Er baut das atomare Increment, das die Datenbank dir nicht gab, aus der einen Garantie wieder auf, die sie gab.

Wenn du lieber auf einem Identitätsanbieter laufen möchtest, der bereits davon ausgeht, dass der Angreifer parallel mitzählt: Authagonal hat diese Kontrollen so gebaut, dass sie alle auf einmal getroffen werden, denn das war ohnehin die einzige Art, wie sie jemals im Ernstfall getroffen werden würden.