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.
Inhalt dieses Beitrags
- Problem
- Warum das rechtlich relevant ist
- Warum das sicherheitsrelevant ist
- Wo Enumeration konkret passiert
- Kurze Antwort
- Was man dagegen tun kann
- Wie Dernium es umsetzt
- Passkey-Login: anonyme Challenge über Discoverable Credentials
- Magic-Link: gleicher Code-Pfad, gleiche DB-Arbeit, gleiche Antwortzeit
- Kein separater Register-Pfad (technisch)
- Abgelehnte Alternativen
- Was Sie jetzt tun sollten
- Wie Dernium hier hilft
- Verifikation
- Offene Punkte
- Häufige Fragen
- Ist es nicht nutzerfreundlicher, klar zu sagen, dass eine Adresse nicht registriert ist?
- Reicht es nicht, einfach die Fehlermeldungen anzugleichen?
- Was ist Timing-Enumeration und warum sind Zufallsverzögerungen dagegen schwach?
- Schützen Passkeys automatisch vor Enumeration?
- Wie kann ich meine eigene Login-Seite testen?
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, das systematische Abfragen, welche Konten existieren). Es ist eine der häufigsten Schwachstellen in produktiven Web-Applikationen und steht seit Jahren auf der OWASP-Liste für Authentifizierungsfehler (OWASP, eine gemeinnützige Stiftung, die Standards zur Web-Sicherheit pflegt).
Warum das rechtlich relevant ist
Eine E-Mail-Adresse ist ein personenbezogenes Datum nach Art. 4 Nr. 1 DSGVO (Datenschutz-Grundverordnung), 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 (systematisches Durchprobieren von Passwörtern), Credential-Stuffing (automatisiertes Ausprobieren bereits geleakter Anmeldedaten aus anderen Diensten) 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: WebAuthn (der Web-Standard hinter Passkeys) erlaubt es dem naheliegenden Passkey-Pfad, den Server nach der Liste der registrierten Anmelde-Schlüssel (Credentials) einer Adresse zu fragen. 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 (rechenaufwendige Verschlüsselung des Passworts) 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 (Schutz-Token gegen Anfragen-Fälschung), einen Set-Cookie-Header.
- Spätere Seiten: eine abweichende Fehlerseite nach Klick auf einen ungültigen Magic-Link-Token (Anmeldung per Einmal-Link in einer E-Mail statt per Passwort).
Für wen ist das? Für Entwickler und Betreiber von Anmeldesystemen.
Kurze Antwort
Eine Login-Seite darf nicht erkennbar machen, ob eine E-Mail-Adresse bei Ihnen registriert ist. Das gelingt nur, wenn die E-Mail-Adresse keine Weiche für beobachtbare Unterschiede stellt - im Text, im Timing und in jedem Header.
- Existierendes und nicht existierendes Konto liefern identische Antwort: gleicher Status, gleicher Body, gleiche Header.
- Teure Arbeit (Mailversand, Passwort-Hash) wird hinter die Antwort verschoben, damit die Antwortzeit nicht verrät, ob etwas passiert ist.
- Bei Passkeys verraten Discoverable Credentials (auf dem Gerät auffindbare Anmelde-Schlüssel) die Identität erst nach der signierten Antwort, nicht davor.
- Rate-Limits (Begrenzung der Anfragen pro Quelle und pro Adresse) bremsen massenhaftes Scannen.
- Dernium setzt das in seinem gemeinsamen Authentifizierungs-Dienst durch; die Eigenschaften gelten für alle Produkte einheitlich.
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" (kleine künstliche Zufallsverzögerung), 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 demuserHandle(einer Konto-Kennung in der signierten Antwort) in der signierten Assertion (der vom Gerät signierten Anmelde-Bestätigung), 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 (der gemeinsame Authentifizierungs-Dienst) 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 (eine Login-Variante, bei der der Browser passende Passkeys von sich aus anbietet) in Kombination mit Discoverable Credentials. Die Challenge (eine zufällige Aufgabe, die das Gerät signieren muss) 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 (eine Warteschlange für ausgehende Mails; bei Cooldown stattdessen 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, der die Queue abarbeitet und Mails sendet. Dieser Worker kommt mit der 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 (also zum Zustellen massenhafter unerwünschter Mails missbraucht wird). 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 (Einweg-Prüfsummen), 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. TOTP (zeitbasierte Einmal-Codes aus einer Authenticator-App) käme ebenso wie SMS ohne E-Mail-Lookup aus, hat aber eigene Schwächen (SS7-Abfang über Lücken im Telefonnetz, 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-Maßnahme. Funktional, aber kein sauberes Design. Jitter ist ein Pflaster: er mittelt die Verteilung weich, löst 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.
Was Sie jetzt tun sollten
Wenn Sie selbst eine Login-Strecke betreiben oder eine bewerten, lohnen sich diese Schritte:
- Drei Antworten vergleichen. Schicken Sie an Login, Registrierung und Passwort-Reset je eine bekannte und eine unbekannte Adresse und prüfen Sie Status, Text, Header und Antwortzeit auf Unterschiede.
- Teure Arbeit nach hinten verlagern. Verschieben Sie Mailversand und Passwort-Hash hinter die Antwort in einen Hintergrund-Worker, damit kein Pfad messbar langsamer ist.
- Rate-Limits setzen. Begrenzen Sie Anfragen pro Quell-IP und pro Zieladresse und speichern Sie dabei nur Prüfsummen, nicht die Klardaten.
- Bei Passkeys auf Discoverable Credentials umstellen. Vermeiden Sie
allowCredentials-Listen, die die Kontoexistenz vor der Challenge verraten. - Fehlertexte vereinheitlichen. Ein generischer Fehler für alle Fälle, statt "Konto unbekannt" gegen "Passwort falsch" zu unterscheiden.
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. Wenn Sie Ihre eigene Anmelde-Infrastruktur prüfen, helfen die kostenlosen Webtools bei der heiklen Konfiguration ringsherum: Ein OIDC-Discovery-Validator, ein JWKS-Validator und ein WebFinger-Check decken Fehlkonfigurationen auf, die solche Seitenkanäle erst öffnen.
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 (eine Browser-Abfrage, ob der automatische Passkey-Vorschlag verfügbar ist) 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. Die Challenge hat eine begrenzte Gültigkeitsdauer (TTL, time to live). 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.
Häufige Fragen
Ist es nicht nutzerfreundlicher, klar zu sagen, dass eine Adresse nicht registriert ist?
Kurzfristig wirkt das hilfreicher, öffnet aber genau die Tür, um die es hier geht: Jeder Dritte kann dann automatisiert herausfinden, wer bei Ihnen Kunde ist. Den Komfort kann man anders herstellen, etwa mit einem Hinweis, dass beim ersten Bestätigen des Mail-Links automatisch ein Konto angelegt wird. So bleibt die UX verständlich, ohne die Kontoexistenz preiszugeben.
Reicht es nicht, einfach die Fehlermeldungen anzugleichen?
Nein. Gleiche Texte sind nur die sichtbarste Ebene. Verräterisch sind auch unterschiedliche Antwortzeiten, abweichende Header, zusätzliche Felder oder eine andere Fehlerseite auf einer Folgestufe. Erst wenn alle diese Kanäle für bekannte und unbekannte Adressen gleich aussehen, ist die Enumeration geschlossen.
Was ist Timing-Enumeration und warum sind Zufallsverzögerungen dagegen schwach?
Bei Timing-Enumeration misst ein Angreifer, wie lange die Antwort dauert: Ein bekanntes Konto löst oft teure Arbeit aus (Mailversand, Passwort-Hash), ein unbekanntes nicht. Zufallsverzögerungen (Jitter) verwischen diesen Unterschied nur statistisch; mit genügend Messungen lässt er sich wieder herausrechnen. Sauberer ist, in jedem Pfad dieselbe Arbeit zu tun und alles Teure hinter die Antwort zu verschieben.
Schützen Passkeys automatisch vor Enumeration?
Nicht automatisch. Der gängige Passkey-Aufbau fragt den Server vorab nach den Anmelde-Schlüsseln einer Adresse und verrät damit, ob das Konto existiert. Erst Discoverable Credentials, bei denen die Identität aus der signierten Antwort kommt, vermeiden diesen Vorab-Lookup. Wer Passkeys einführt, muss diesen Punkt aktiv richtig umsetzen.
Wie kann ich meine eigene Login-Seite testen?
Schicken Sie dieselbe Anfrage einmal mit einer Adresse, die sicher existiert, und einmal mit einer, die sicher nicht existiert, und vergleichen Sie HTTP-Status, Antworttext, alle Header und die Antwortzeit. Wiederholen Sie das für Login, Registrierung und Passwort-Reset. Jeder systematische Unterschied zwischen den beiden Fällen ist ein Enumerations-Leck.