Login ohne Konten-Enumeration: die häufigste Privacy-Falle im Web
Viele Login-Seiten verraten ungewollt, wer Kunde ist. Warum das ein DSGVO-Problem ist, wie man es in allen Schichten vermeidet und wie der Dernium-Login von der ersten Abfrage bis zur kryptografischen Verifikation keine Konten-Existenz preisgibt.
Das Problem
Fast jede Login-Seite im Internet verrät ungewollt, ob eine E-Mail-Adresse bei einem Dienst registriert ist. Die Unterscheidung zwischen "Diese Adresse kennen wir nicht" und "Passwort falsch", ein zusätzliches Antwortfeld nur bei bekannten Adressen, eine andere Antwortzeit, eine andere Fehlerseite beim Passwort-Zurücksetzen: jede dieser Asymmetrien ist ein Leck.
Das Muster hat einen Namen: Benutzer-Enumeration (engl. user enumeration). Es ist eine der häufigsten Schwachstellen in produktiven Web-Applikationen und steht seit Jahren auf der OWASP-Liste für Authentifizierungsfehler.
Warum das rechtlich relevant ist
Eine E-Mail-Adresse ist ein personenbezogenes Datum nach Art. 4 Nr. 1 DSGVO, weil sie eine natürliche Person identifizierbar macht. Die Information "Adresse X ist Kunde von Dienst Y" ist ebenfalls personenbezogen: sie sagt etwas über das Verhalten und die Geschäftsbeziehungen der Person aus.
Wer eine Login-Seite so baut, dass beliebige Dritte diese Information automatisiert abfragen können, verarbeitet und offenbart personenbezogene Daten an Unbefugte. Für diese Offenbarung fehlt in den meisten Fällen eine Rechtsgrundlage nach Art. 6 DSGVO; eine Einwilligung der betroffenen Person existiert nicht, und die "Offenlegung an jeden Anfrager" lässt sich auch nicht plausibel als berechtigtes Interesse konstruieren.
Warum das sicherheitsrelevant ist
Enumeration ist nicht selbstzweckhaft. Sie ist Vorbereitung:
- Ein Angreifer scannt eine Adressliste, etwa aus einem fremden Leak, gegen die Login-Seite eines beliebigen Anbieters. Er behält nur die Adressen, die der Anbieter als "existiert" bestätigt.
- Auf dieser gefilterten Liste konzentriert er dann Brute-Force-Versuche, Credential-Stuffing mit Passwörtern aus anderen Leaks oder gezieltes Phishing: "Sehr geehrter Kunde von X, Ihr Konto erfordert eine Aktion."
- Gezielte Angriffe haben nach industrieüblichen Messungen eine drei- bis vierfach höhere Erfolgsquote als ungezielte.
Enumeration senkt also unmittelbar den Aufwand und erhöht die Wirksamkeit späterer Angriffe. Sie ist die Zielinformation, die das Spätere überhaupt ökonomisch macht.
Wo Enumeration konkret passiert
Sie versteckt sich selten an der offensichtlichen Stelle allein. Typische Lecks:
- Login-Fehlertext: "Konto nicht gefunden" versus "Passwort falsch".
- Registrierung: "Diese Adresse ist bereits registriert" als Validierungsfehler.
- Passwort-Zurücksetzen: "Wir haben Ihnen eine Mail gesendet" versus "Diese Adresse ist uns nicht bekannt".
- WebAuthn mit
allowCredentials: der naheliegende Passkey-Pfad fragt den Server nach der Liste der registrierten Credentials einer Adresse. Liefert er eine Liste, existiert der Account; liefert er eine leere Antwort, existiert er nicht. - Timing: für bekannte Adressen läuft ein Mail-Send, ein Passwort-Hash oder ein Datenbank-Join; für unbekannte geht der Pfad in Millisekunden zurück. Der Unterschied ist messbar.
- Response-Größe und -Header: bekannte Konten liefern zusätzliche Felder, ein gesetztes CSRF-Token, einen Set-Cookie-Header.
- Spätere Seiten: eine abweichende Fehlerseite nach Klick auf einen ungültigen Magic-Link-Token.
Was man dagegen tun kann
Das Muster ist: die E-Mail-Adresse darf nicht die Weiche für beobachtbare Unterschiede stellen. Einzelne Fehlermeldungen zu glätten reicht nicht. Saubere Umsetzung erfordert Disziplin auf mehreren Ebenen:
- Identische Antwort bei existierendem und nicht existierendem Konto. Gleicher HTTP-Status, gleicher Body, gleiche Header, gleiches Redirect-Ziel.
- Strukturell gleiches Timing. Nicht "Konstante + Jitter als Pflaster", sondern: jeder Pfad macht in der Datenbank denselben Arbeitsschritt, und alles, was teuer ist (SMTP, Passwort-Hash), passiert nach der Antwort auf einem Hintergrund-Worker. Dann ist die Antwortzeit unabhängig davon, welcher Pfad gewählt wurde - und zwar nicht nur statistisch, sondern strukturell.
- Rate-Limits. Enumeration über das Internet skaliert nur, wenn der Anbieter beliebig viele Abfragen zulässt. Ein striktes IP- und Adress-Limit reduziert den Scanning-Durchsatz massiv.
- Keine User-bezogenen Daten vor der Verifikation. Der Server darf vor einer kryptografischen Prüfung (Signatur, Token-Hash) keinen User-spezifischen Lookup machen, der sich in der Antwort niederschlägt.
- Discoverable Credentials statt
allowCredentialsbei Passkeys. Die Identität kommt dann aus demuserHandlein der signierten Assertion, nicht aus einem Pre-Lookup. - Einheitliche Fehlerpfade nach fehlgeschlagener Verifikation. Nicht "Kein Passkey für diese Adresse" versus "Signatur ungültig", sondern in beiden Fällen derselbe generische Fehler.
Wie Dernium es umsetzt
Der Dernium-Auth-Service ist durchgängig datenschutzkonform auf Nicht-Enumerierbarkeit ausgelegt. Zwei Endpunkte sind die Hauptangriffsfläche; beide verhalten sich nach diesem Prinzip.
Passkey-Login: anonyme Challenge über Discoverable Credentials
Der Passkey-Pfad nutzt WebAuthn Conditional UI in Kombination mit Discoverable Credentials. Die Challenge wird ohne Kontobezug erzeugt; der Server erfährt erst nach der signierten Assertion, um welches Konto es geht. Der Browser schlägt dem Nutzer über den Autofill an, welche Passkeys lokal verfügbar sind; der Server ist an der Entscheidung nicht beteiligt.
Früher lag vor diesem Pfad noch ein POST /v1/login/start-Endpunkt, der eine statische Antwort über verfügbare Methoden zurückgab. Der Endpunkt tat auf Serverseite exakt nichts und wurde vom Browser auch nicht mehr aufgerufen. Er ist entfernt, damit die öffentliche API-Oberfläche kleiner bleibt.
Magic-Link: gleicher Code-Pfad, gleiche DB-Arbeit, gleiche Antwortzeit
Der Magic-Link-Pfad ist der zweite Haupt-Angriffsvektor für Enumeration. Dernium trennt hier zwei Dinge sauber:
Die HTTP-Annahme (POST /v1/login/magic/send) prüft nur das IP-Rate-Limit, sucht den User und schreibt genau eine Zeile in eine Mail-Outbox (bzw. - bei Cooldown - einen Audit-Eintrag derselben Form). Sie antwortet immer mit HTTP 202 und {"message":"check-email"}. Kein SMTP, kein Passwort-Hash, kein DNS-Lookup. Die Antwortzeit ist für alle drei internen Pfade (bekannter verifizierter User, bekannter unverifizierter User, unbekannte Adresse) identisch aufgebaut: IP-Check, ein SELECT, ein INSERT.
Der Mail-Versand läuft in einem Hintergrund-Worker (auth-service-internes outbox-Modul), der die Queue abarbeitet und Mails sendet. Dieser Worker kommt mit dem Endanwendung nicht in Kontakt.
Dieses Design hat einen praktischen Nebeneffekt: zuvor setzte der Dienst künstliche Jitter-Delays (30-120 ms Basisjitter, 180-520 ms Silent-Jitter im Cooldown), damit die Antwortzeit-Verteilung nicht zwischen "Mail geht raus" und "nichts passiert" unterscheidet. Weil jetzt strukturell nichts mehr passiert, das langsam sein könnte, sind diese Jitter ersatzlos entfernt. Die Antwortzeit ist in allen Pfaden die reine DB-Roundtrip-Zeit. Das ist robust und nicht trickreich.
Der Cooldown für Einladungsmails ist dabei auch eine Anti-Missbrauchsmaßnahme: er verhindert, dass der Dienst als Mail-Bombing-Relay dient. Ein Angreifer, der versucht, einen Dritten mit wiederholten Einladungen zu fluten, läuft nach der ersten Mail auf eine Sperre. Von außen bleibt das weiter ununterscheidbar, er sieht in jedem Fall dasselbe 202.
Dazu kommen zwei Rate-Limits: eines pro Quell-IP gegen massenhafte Anfragen, eines pro Adresse im Login-Pfad gegen Mail-Bombing einzelner Konten. Gespeichert werden jeweils nur die Hash-Werte, nicht die Klardaten (IP-Adresse, Mailadresse).
Kein separater Register-Pfad (technisch)
In der UI könnten wir eine getrennte "Registrieren"-Seite anbieten, wenn sie bitgleich dasselbe HTTP-Verhalten triggert (selber Endpoint, selber Body, selbe Antwort). Das wäre rein kosmetisch. Die aktuelle Login-Seite verweist explizit darauf, dass beim ersten Bestätigen des Mail-Links das Konto angelegt wird; damit ist die UX-Lücke gering und der Code-Pfad bleibt einfach - und damit besser wartbar und weniger fehleranfällig.
Abgelehnte Alternativen
Passkey-API mit E-Mail-Parameter. Der Weg, den die meisten WebAuthn-Tutorials zeigen: Client schickt die Adresse an login/start, Server lookupt die registrierten Credentials und liefert eine allowCredentials-Liste. Einfacher zu verdrahten, aber verrät die Kontoexistenz noch vor der Challenge. Für uns keine Option.
Unified Dummy-Response beim Login. Auch für unbekannte Adressen eine syntaktisch valide Challenge zurückgeben, auf die der Authenticator dann nicht reagiert. Technisch möglich, aber die UX leidet: jeder Erstbesucher sieht einen Passkey-Dialog, bevor der Magic-Link-Pfad angeboten wird. Discoverable Credentials lösen das eleganter.
SMS/TOTP als Primärfaktor. Käme ohne E-Mail-Lookup aus, hat aber eigene Schwächen (SS7-Abfang, nicht phishingresistent, außerdem potenziell kostenintensiv auf unserer Seite) und, am allerschlimmsten, setzt Telefonnummern als personenbezogene Daten voraus. In unseren Augen wäre das ein Rückschritt.
Jitter-Delays als Anti-Timing-Massnahme. Funktional, aber kein sauberes Design. Jitter ist ein Pflaster: er mittelt die Verteilung weich, loest aber nicht die Ursache (unterschiedliche Arbeit in verschiedenen Pfaden). Bei hinreichend vielen Samples ist ein Jitter-Mittel immer noch vom Median eines "nichts-passiert"-Pfads trennbar. Die Queue-basierte Architektur ist sauberer.
Wie Dernium hier hilft
Die Authentifizierungs-Schicht wird von allen Dernium-Produkten gemeinsam genutzt; die Anti-Enumerations-Eigenschaften gelten damit für die gesamte Produktpalette einheitlich.
Verifikation
Die Behauptungen in diesem Beitrag sind unabhängig prüfbar:
POST https://auth.dernium.de/v1/login/magic/sendmit einer bekannten und einer unbekannten Adresse liefert jeweils HTTP 202 mit Body{"message":"check-email"}. Laufzeit-Messungen liegen im Bereich der reinen DB-Roundtrips (~1-10 ms am Prozess, abhaengig vom Netz) und unterscheiden sich nicht systematisch zwischen den Pfaden.POST https://auth.dernium.de/v1/login/passkey/startakzeptiert keinen Body-Parameter; die Response enthältchallenge_idundoptions, aber keine user-bezogenen Felder.POST https://auth.dernium.de/v1/login/passkey/finishmit einer gefälschten Assertion schlägt mit generischem401 Unauthorizedfehl, ohne Angabe, ob deruserHandlebekannt war oder z. B. die Signatur nicht stimmte.POST https://auth.dernium.de/v1/login/startliefert404: der früher statische Endpunkt ist entfernt.- Die
webauthn-rs-Bibliothek hinter der Implementierung ist quelloffen unter Apache-2.0, ihre Funktionstart_discoverable_authenticationin der Rust-Dokumentation verlinkt.
Offene Punkte
Conditional UI braucht einen modernen Browser. isConditionalMediationAvailable kennt die Chromium-Familie, Safari 16+ und Firefox 119+. Ältere Browser fallen stillschweigend auf den manuellen "Mit Passkey anmelden"-Button oder den Magic-Link-Pfad zurück. Es gibt keinen sauberen Mechanismus, das serverseitig zu erkennen; der Server bleibt für alle Clients gleich stumm.
Challenge-TTL im Minutenbereich. Tabs, die lange unbeobachtet bleiben, haben nach Ablauf eine stille Challenge. Das Frontend retryt, sobald Interaktion beginnt. Wir beobachten, ob das reicht, oder ob die TTL angepasst werden muss.