Häufige JWT-Authentifizierungsfehler in der Produktion
Algorithmus-Verwechslung, schwache Geheimnisse, fehlende Ablaufzeit und die Bibliotheks-Defaults, die sich unter all den Ratschlägen still geändert haben.
JWT-Fehler sind seit einem Jahrzehnt dokumentiert und gehen trotzdem weiter in Produktion. Nicht weil die Fehlerbilder schwer zu beschreiben wären, sondern weil die richtige Antwort davon abhängt, welche Bibliothek Sie haben, in welcher Version, und was diese Version tut, wenn Sie eine Option weglassen. Ein Ratschlag von 2018 über eine Bibliothek, die seither ihre Defaults geändert hat, ist schlechter als gar keiner, weil er konkret genug ist, um geglaubt zu werden.
Also hier der aktuelle Stand, mit den geänderten Stellen als geändert markiert.
Die Form der Sache
Ein JWT sind drei Base64URL-Segmente, getrennt durch Punkte. Header, Payload, Signatur. Der Header nennt den Algorithmus, der Payload trägt die Claims, und die Signatur deckt die ersten beiden ab, verbunden durch einen Punkt.
Der Payload ist kodiert, nicht verschlüsselt. Wer den Token hat, kann ihn lesen, was unser JWT-Decoder an jedem eingefügten Token vorführt, ohne Schlüssel und ohne irgendetwas zu senden.
Beide klassischen Angriffe haben dieselbe Wurzel: Der Header ist unvertrauenswürdige Eingabe, und manche Prüfer haben ihn benutzt, um zu entscheiden, wie geprüft wird.
Der Algorithmus none, und wer wirklich schuld war
Der Angriff ist alt und einfach. Nehmen Sie einen echten Token, schreiben Sie den Payload zu {"sub": "admin"} um, setzen Sie den Header auf {"alg": "none"}, lassen Sie die Signatur leer, senden Sie ab. Ein Prüfer, der dem Header vertraut, sieht einen Algorithmus mit der Bedeutung "keine Prüfung nötig" und gehorcht3.
Üblicherweise wird dafür die Spezifikation verantwortlich gemacht. Das steht dort aber nicht. RFC 7518 verlangt, dass eine Implementierung ein unsigniertes JWS nur dann als gültig akzeptiert, wenn die Anwendung das für genau dieses Objekt verlangt hat, und sagt ausdrücklich, dass Implementierungen sie nicht standardmässig akzeptieren dürfen2. Die Bibliotheken, die es taten, folgten keiner grosszügigen Spezifikation. Sie ignorierten eine strenge.
Die bekannten Bibliotheken haben das vor einem Jahrzehnt behoben. Übergeben Sie die Algorithmusliste trotzdem:
jwt.verify(token, secret, { algorithms: ['HS256'] });
Nicht weil Ihre Bibliothek vermutlich kaputt ist, sondern weil diese Zeile eine Zusicherung ist, die man im Review lesen kann. Die Alternative wäre, die Defaults jeder Bibliothek im Abhängigkeitsbaum zu kennen.
Algorithmus-Verwechslung
Ihr Server signiert mit RS256 und veröffentlicht den öffentlichen RSA-Schlüssel, wie es sich gehört. Ein Angreifer nimmt diesen Schlüssel, schreibt einen Token mit "alg": "HS256" und signiert ihn per HMAC mit den Bytes des öffentlichen Schlüssels als geteiltem Geheimnis. Ein Prüfer, der alg aus dem Header liest, lädt "den Schlüssel", reicht ihn an seinen HMAC-Pfad, und die Signatur passt3.
Der veröffentlichte Schlüssel sollte nie ein Geheimnis sein. Der Fehler ist, dass der Token entscheiden durfte, welche Rolle er spielt.
Was sich geändert hat: jsonwebtoken leitet ohne die Option algorithms inzwischen eine Positivliste aus dem übergebenen Schlüssel ab, ein Secret bekommt also die HS-Familie und ein RSA-Schlüssel die RS-Familie4. Das schliesst beide Angriffe in dieser Bibliothek standardmässig. Es schliesst sie nicht in Code, der alg selbst liest, um einen Schlüssel auszuwählen, ein Muster, das man beim Jonglieren mehrerer Aussteller weiterhin von Hand schreibt.
Geheimnisse, die keine sind
HS256 ist HMAC-SHA256 und exakt so stark wie der Schlüssel. RFC 7518 setzt eine Untergrenze: ein Schlüssel mindestens in Grösse der Hash-Ausgabe, also 256 Bit, und formuliert das als MUSS2.
Das Versagen ist nie ein etwas zu kurzer Zufallsschlüssel. Es ist ein Wort. Ein JWT_SECRET, das jemand beim Aufsetzen einer Staging-Umgebung auf changeme gesetzt hat und das dasselbe Deploy-Skript nach Produktion getragen hat.
Sehen Sie, was das wert ist. Als Zufallsstring sind acht Kleinbuchstaben etwa 37,6 Bit, schon das ist schwach. Aber changeme ist kein Zufallsstring, sondern ein Wortlisteneintrag, die echte Zahl liegt also näher an der Position dieses Wortes in rockyou.txt. hashcat hat einen eigenen JWT-Modus, 165005, und ein mitgeschnittener Token ist die ganze benötigte Eingabe. Das ist ein Offline-Angriff. Nichts begrenzt die Rate, und nichts protokolliert ihn.
Erzeugen Sie den Schlüssel aus einer kryptografischen Quelle und tippen Sie ihn nie selbst:
openssl rand -base64 32
Unser Zufalls-String-Generator macht dasselbe im Browser mit crypto.getRandomValues und Verwerfungsstichprobe, sodass die Zeichenverteilung nicht durch einen Modulo verzerrt wird. Wer sehen will, was ein Kandidat tatsächlich wert ist, bevor er ihm vertraut, bekommt vom Passwort-Entropie-Rechner eine Zahl dafür.
Danach behandeln Sie es wie ein Datenbank-Passwort: Secret Manager, Rotationsplan, nie in Git.
Ablauf, und das Widerrufsproblem dahinter
exp ist der Zeitpunkt, ab dem der Token nicht mehr akzeptiert werden darf1. Ein ohne exp ausgestellter Token gilt, bis Sie den Signaturschlüssel wechseln, was jeden je ausgestellten Token für alle auf einmal ungültig macht. Das ist kein Widerrufsmechanismus, das ist ein Ausfall.
Vernünftige Fenster: fünfzehn Minuten für API-Access-Tokens, Stunden bis Wochen für Refresh Tokens, die an einem widerrufbaren Ort liegen, höchstens eine Stunde für Einmal-Links wie Passwort-Resets.
Damit ist die Widerrufsgeschichte im Grunde erzählt. Ein Prüfer, der eine Signatur und einen Zeitstempel prüft, kann nicht hören, dass Sie es sich anders überlegt haben. Jede Sperr- oder Positivliste führt genau den Datenbankzugriff wieder ein, den Zustandslosigkeit loswerden sollte. Kurze Access Tokens plus ein Refresh Token in der Datenbank sind keine elegante Antwort, sondern die funktionierende: Was Sie nicht widerrufen können, gilt nur kurz, und was lange gilt, können Sie löschen.
Bietet Ihre Bibliothek eine Option, die Ablaufprüfung zu überspringen, ist sie für Tests da. In Produktion hat sie keinen Zweck.
Uhrenabweichung, ohne Folklore
Aussteller und Prüfer sind verschiedene Maschinen mit verschiedenen Uhren, ein eben ausgestellter Token kann also aussehen, als begänne er in der Zukunft. Bibliotheken nehmen ein clockTolerance in Sekunden, angewandt auf nbf und exp4.
Schalten Sie es nicht ab, setzen Sie es nicht auf eine Stunde, und gehen Sie nicht von einem hilfreichen Default aus. RFC 7519 nennt den vernünftigen Bereich schriftlich: eine kleine Toleranz, üblicherweise nicht mehr als ein paar Minuten1. Setzen Sie es ausdrücklich auf etwa dreissig Sekunden und lassen Sie NTP laufen, damit Sie nie mehr brauchen.
Wo der Token wohnt
Ein JWT in localStorage ist für jedes Skript auf der Seite lesbar. Ihr Analytics-Anbieter, das Chat-Widget, ein A/B-Test-Snippet, jede XSS-Payload, jede Erweiterung mit Host-Berechtigung. Alle können ihn lesen, und ein Bearer Token funktioniert für den, der ihn hat.
Ein Cookie mit HttpOnly, Secure und SameSite behebt das Kopieren, nicht den Missbrauch. Skript, das auf Ihrer Seite läuft, kann weiterhin authentifizierte Anfragen als die Nutzerin stellen, weil der Browser das Cookie anhängt. Was es nicht kann, ist den Token auslesen und irgendwohin senden, um ihn morgen von einer anderen Maschine aus zu verwenden, nachdem Sie die Lücke geschlossen haben. Dieser Unterschied ist die Migration wert.
Dafür wird CSRF Ihr Problem, behandelt mit einem eigenen Token oder dem Double-Submit-Muster. Haben Sie ein localStorage-System geerbt und können Auth in diesem Quartal nicht umbauen, ist eine strikte Content Security Policy der Zwischenschritt. Sie macht XSS nicht harmlos, sie macht die Ausnutzung aufwendiger.
Claims, die keine sein sollten
Der Payload ist für jeden lesbar, der den Token hat, er sollte also das Minimum enthalten, das zum Routen einer Anfrage nötig ist: Subject, Aussteller, Zielgruppe, Ablauf, grobe Scopes. Keine internen Bezeichner, die Sie sonst nirgends preisgeben, keine personenbezogenen Daten, keine Rollennamen, die Ihre Infrastruktur beschreiben. Alles andere liegt in der Datenbank und wird pro Anfrage geladen.
Und prüfen Sie iss und aud. RFC 7519 ist bei der Zielgruppe deutlich: Findet sich der verarbeitende Principal nicht in aud, ist der Token abzulehnen1. Zwei Dienste mit einem gemeinsamen Signaturschlüssel und ohne Audience-Prüfung heisst, dass ein für die Admin-Konsole ausgestellter Token ein gültiger Token für die API ist.
jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'https://auth.example.com/',
audience: 'https://api.example.com/',
});
Die Checkliste
- Explizites
algorithmsbei jedem Verify, egal welche Defaults Ihre Bibliothek heute hat. - 256 Bit Schlüsselmaterial für HS256, aus einem CSPRNG, nie eine Phrase.
expauf jedem Token, kurz, ohne Ausnahmen der Bequemlichkeit halber.issundaudgeprüft, nicht bloss vorhanden.clockToleranceausdrücklich gesetzt, in Sekunden.- Speicherung im HttpOnly-Cookie, ausser Sie haben einen konkreten Grund und eine dokumentierte Gegenmassnahme.
- Payload auf das reduziert, was das Routing braucht.
- Eine Widerrufsantwort, die existiert, bevor Sie sie brauchen.
Zum Ansehen eines Tokens beim Debuggen läuft der JWT-Decoder lokal, es wird nichts hochgeladen. Der HMAC-Generator deckt HS256, HS384 und HS512 ab, wenn Sie eine Signatur von Hand nachrechnen wollen. Alles darüber hinaus gehört in Code, unter Test.
Die wiederkehrende Lehre ist nicht, dass JWTs gefährlich sind. Sie ist, dass "die Bibliothek erledigt das" eine Aussage mit einer Versionsnummer daran ist, und nur die Version zählt, die bei Ihnen läuft.
Quellen
Jede Zahl in diesem Artikel lässt sich auf eine Quelle unten zurückführen. Behauptungen ohne Beleg wurden gestrichen, nicht abgeschwächt.
- PrimärquelleIETF
Die Definitionen der Claims iss, aud und exp, dass ein JWT abzulehnen ist, wenn sich der verarbeitende Principal nicht in aud wiederfindet, und der Hinweis, dass Implementierer eine kleine Toleranz für Uhrenabweichung vorsehen dürfen, üblicherweise nicht mehr als ein paar Minuten.
- PrimärquelleIETF
Abschnitt 3.2, dass mit HMAC ein Schlüssel mindestens in Grösse der Hash-Ausgabe verwendet werden MUSS, und Abschnitt 3.6, dass Implementierungen unsignierte JWS nicht standardmässig akzeptieren dürfen.
- PrimärquelleAuth0
Die ursprüngliche Offenlegung sowohl der alg-none-Umgehung als auch des Verwechslungsangriffs RS256 zu HS256, samt der Erklärung, dass ein auf RSA eingestellter Server den öffentlichen Schlüssel als HMAC-Geheimnis behandelt.
- PrimärquelleGitHub
Dass die Bibliothek ohne die Option algorithms inzwischen eine Positivliste aus dem Schlüsseltyp ableitet, und die Beschreibung von clockTolerance als Sekunden Toleranz bei nbf und exp.
- Primärquellehashcat
Dass der Hash-Modus 16500 JWT (JSON Web Token) ist, womit ein mitgeschnittener Token zum Ziel eines Offline-Angriffs wird.
Themen
- JWT
- Authentication
- Security
- Tokens
- Hmac
In diesem Artikel erwähnte Tools
- JWT Decoder - Decode and inspect JWT tokens — header, payload and claims.
- HMAC Generator - Generate HMAC signatures with SHA-1, SHA-256, SHA-384 and SHA-512.
- Random String Generator - Generate random strings with configurable length and character set.
- Password Entropy Calculator - Estimate password entropy, character pool size and crack-time ranges for online and offline attacks.
Neue Tools per E-Mail
Neue Tools und gelegentlich ein ausführlicher Artikel, etwa einmal im Monat. Kein Spam, deine Adresse bekommt niemand, Abmeldung mit einem Klick.