JWT-Fehler, die zu Datenlecks werden

Ein JSON Web Token ist eine signierte Erklärung, kein versiegelter Umschlag. Es besteht aus drei durch Punkte verbundenen Base64url-kodierten Segmenten: einem Header, der den Signaturalgorithmus nennt, einer Payload mit Claims über einen Benutzer oder eine Sitzung, und einer Signatur über die ersten beiden. Die Signatur beweist, dass das Token von dem ausgestellt wurde, der den Schlüssel besitzt, und seitdem nicht verändert wurde. Über Geheimhaltung beweist sie nichts: Jeder, der das Token besitzt, kann die Payload dekodieren und jeden Claim darin lesen — deshalb braucht das Dekodieren ein Einfügen, kein Passwort. Wenn JWTs in Produktion versagen, ist fast nie die Kryptografie zerbrochen. Zerbrochen ist eine Prüfung, die jemand ausgelassen hat — der Algorithmus, dem aus dem Token selbst vertraut wurde, die Ablaufzeit, die niemand validierte, das gemeinsame Geheimnis, das ein Mensch aus dem Gedächtnis hätte tippen können. Fügen Sie ein beliebiges Token in den JWT-Decoder ein, und Header und Claims liegen offen da. Dieser Artikel geht die Ingenieursfehler durch, die aus ausgelassenen Prüfungen Lecks machen, und die defensive Regel, die jeden davon schließt.

Signiert ist nicht dasselbe wie geheim

Die Payload ist Base64url — eine Kodierung aus derselben Familie wie gewöhnliches Base64 —, keine Verschlüsselung. Alles darin ist für jeden lesbar, durch dessen Hände das Token geht: E-Mail-Adressen, Rollen, Tenant-IDs, interne Kontonummern und gelegentlich weit Schlimmeres, etwa Passwort-Zurücksetzungs-Flags oder Fragmente personenbezogener Daten, die ein Entwickler zum Debuggen hinzugefügt und nie entfernt hat. Tokens reisen auch weiter, als Entwickler sich vorstellen: Sie liegen im Browser-Speicher, durchqueren Proxy- und Anwendungsprotokolle, werden in Absturzberichte kopiert und in Fehlertickets und Chatverläufe eingefügt. Die Arbeitsregel folgt direkt daraus: Nehmen Sie nie einen Claim in ein Token auf, bei dem es Ihnen unangenehm wäre, ihn in einer Protokolldatei gedruckt zu sehen, denn früher oder später wird er dort stehen.

Das Missverständnis ist verständlich — ein kompaktes Token sieht aus wie Chiffretext, lauter unaussprechliche Zeichen und Punkte. Aber die Signatur macht das Token nur manipulationssicher: Ändern Sie einen Claim, schlägt die Verifizierung fehl. Vertraulichkeit ist eine andere Konstruktion derselben Normenfamilie (verschlüsselte Tokens, JWE), und ein gewöhnliches Zugriffstoken ist sie selten. Entwerfen Sie die Payload also als öffentlichen Text mit einer Signatur darauf: nur Kennungen und Autorisierungsfakten, sensible Daten serverseitig per ID abgerufen, und kurze Lebensdauern, damit das, was doch durchsickert, schnell abläuft.

Das Token wählt seinen eigenen Algorithmus

Der Header deklariert ein alg, und der älteste Fehler im JWT-Buch ist ein Verifizierer, der ihm glaubt. Einer der deklarierten Werte ist none — ein unsigniertes Token, ganz ohne Signatur. Eine Bibliothek oder handgefertigte Prüfung, die dem vom Token selbst deklarierten Algorithmus gehorcht, kann ein Token mit leerer Signatur erhalten und es als echt akzeptieren. Der Best-Practice-RFC ist hier unmissverständlich: Unsignierte Tokens sind nur dort akzeptabel, wo der Kanal selbst die Integrität bereits garantiert — und ein Bearer-Token, das über das öffentliche Internet eintrifft, ist das nicht. Derselbe Fehler, dem Header zu vertrauen, erzeugt die Algorithmus-Verwechslung. Ein System, das RS256-Tokens ausstellt, hält einen privaten Schlüssel und veröffentlicht den passenden öffentlichen Schlüssel zur Verifizierung. Lässt sich der Verifizierer dazu überreden, das Token stattdessen als HS256 zu verarbeiten, wird das Verifizierungsmaterial zum öffentlichen Schlüssel — öffentlich von Grund auf, jedermann bekannt —, der als symmetrisches Geheimnis verwendet wird, und Signaturen, die jeder erzeugen kann, beginnen zu verifizieren. In keinem der beiden Fehler wird ein Schlüssel gestohlen; der Verifizierer folgte schlicht Anweisungen, die der Absender geschrieben hatte.

Der Header trägt ein zweites, leisere Risiko: Schlüsselzeiger. Parameter wie kid (eine Schlüssel-ID), jku und x5u (URLs, unter denen ein Schlüssel oder ein Zertifikat liegt) sagen dem Verifizierer, wo er den Verifizierungsschlüssel findet — und sie kommen im vom Angreifer beeinflussbaren Token an. Fließt ein kid-Wert ungeprüft in einen Dateipfad, eine Datenbankabfrage oder einen Schlüsselabruf, wird die Suche selbst zur Angriffsfläche. Die defensiven Regeln haben alle dieselbe Form: den erwarteten Algorithmus in der Serverkonfiguration festnageln und alles andere ablehnen; Schlüssel aus der eigenen Registrierung auflösen, mit Schlüssel-IDs, die gegen eine Zulassungsliste geprüft werden; niemals Schlüsselmaterial von einer URL abrufen, die das Token liefert; und jedes Header-Feld als nicht vertrauenswürdige Eingabe behandeln, denn das ist es.

Die Signatur war gültig; die Claims wurden nie geprüft

Eine Signaturprüfung beantwortet eine einzige Frage — wurde dieses Token vom Schlüsselinhaber ausgestellt und unverändert gelassen? Sie sagt nichts darüber, ob das Token hier und jetzt gelten sollte. Das ist die Aufgabe der Claims, und die Claim-Validierung ist die Stelle, an der Produktionssysteme still Schritte auslassen. Zuerst exp: Ein Verifizierer, der die Ablaufzeit nie prüft, macht jedes Token zu einer Dauerberechtigung, sodass ein vor drei Jahren in einem Protokoll durchgesickertes Token heute noch authentifiziert. Dann iss: Akzeptieren Sie Tokens nur von Ausstellern, die Sie konfiguriert haben, nicht von jedem, der zufällig einen Algorithmus teilt. Und aud, die teuerste Auslassung: Organisationen betreiben oft mehrere Dienste mit einem einzigen Signaturschlüssel, und das Token eines geringwertigen Dienstes — eine kostenlose API-Stufe, ein Werkzeug nahe der Staging-Umgebung — wird dann zu einem korrekt signierten Token, das ein hochwertiger Dienst akzeptiert, weil die Signatur echt ist und niemand gefragt hat, für welche Audience das Token ausgestellt wurde.

Nichts davon ist exotisch. Es ist eine Checkliste, und der Fehlermodus ist, dass Bibliotheken die Signatur begeistert verifizieren, während sie Claim-Prüfungen optional, deaktiviert oder als Problem des Aufrufers belassen. Die defensive Haltung ist, exp, iss und aud bei jeder Verifizierung zu verlangen, bei allem Fehlenden oder Abweichenden geschlossen zu versagen und Uhrzeitabweichungen in Minuten zu tolerieren, nicht in Stunden — Toleranz existiert für ungenaue Uhren, und weit genug gedehnt belebt sie abgelaufene Tokens still wieder. Dann testen Sie die negativen Fälle so, wie Sie ein Anmeldeformular testen würden: Ein abgelaufenes Token, ein Token mit falscher Audience und eines von einem unbekannten Aussteller sollten alle abgelehnt werden, und diese Ablehnungen verdienen es, als das Sicherheitssignal protokolliert zu werden, das sie sind.

Geheimnisse, die ein Mensch sich merken kann, sind Geheimnisse, die ein Angreifer testen kann

HS256 ist symmetrisch: Ein einziges gemeinsames Geheimnis signiert und verifiziert. In echten Codebasen ist dieses Geheimnis zu oft ein Wort oder eine kurze Phrase — ein Firmenname, ein aus einem Tutorial kopierter Framework-Standardwert, etwas, das ein Entwickler aus dem Gedächtnis tippen könnte. Hier ist das Prinzip, das dies fatal macht, und es ist eine Eigenschaft des Entwurfs, nicht irgendeines Werkzeugs: Ein erfasstes Token trägt alles, was nötig ist, um ein geratenes Geheimnis offline zu testen. Die Nachricht ist bekannt, die Signatur ist bekannt, also können Vermutungen lokal, mit voller Geschwindigkeit, geprüft werden, ohne dass der Server je einen Versuch sieht oder irgendein Ratenlimit greift. Die Sicherheit des gesamten Verfahrens reduziert sich damit auf die Entropie dieser einen Zeichenkette — und von Menschen gewählte Zeichenketten haben sehr wenig davon.

Die Abhilfen sind unspektakulär. Erzeugen Sie Signaturgeheimnisse maschinell, in angemessener Länge — mindestens 256 Bit Entropie für HMAC — und verwahren Sie sie wie die Produktionszugangsdaten, die sie sind. Machen Sie sie pro Umgebung einzigartig, damit das Geheimnis in einer durchgesickerten Staging-Konfiguration in Produktion nichts kauft, und lassen Sie niemals einen Entwicklungs-Standardwert in einen echten Einsatz überleben. Wo viele Dienste Tokens verifizieren, bevorzugen Sie asymmetrische Algorithmen wie RS256 oder ES256: Verifizierer halten dann nur öffentliche Schlüssel, sodass ein kompromittierter Verifizierer nichts durchsickern lässt, womit sich Tokens prägen ließen, und der Signaturschlüssel bleibt an einem einzigen Ort. Und halten Sie eine Rotationsgeschichte bereit, bevor Sie sie brauchen, denn bei einem symmetrischen Geheimnis gibt es keinen Alarm, wenn es durchsickert — das erste Zeichen ist gewöhnlich ein Token, das Sie nicht ausgestellt haben und das als Ihres akzeptiert wird.

Wo der Browser das Token aufbewahrt, entscheidet, wer es lesen kann

Auf der Clientseite ist die Speicherwahl eine Sicherheitsentscheidung, ob jemand sie bewusst getroffen hat oder nicht. Ein Token im localStorage ist für jedes Skript lesbar, das auf der Seite läuft: für Ihren eigenen Code, für jedes Drittanbieter-Tag und Analyse-Snippet, das Sie zugelassen haben — und für jede Cross-Site-Scripting-Nutzlast, die je ausgeführt wird, aus welchem nicht maskierten Kommentarfeld auch immer in irgendeiner Ecke der Anwendung. Ein Bearer-Token ist ein kopierbares Berechtigungsmittel: Wer es liest, kann es von überall her vorzeigen und bis zu seinem Ablauf dieser Benutzer sein. Langlebige Tokens in skriptlesbarem Speicher sind daher exakt so viel wert wie Ihr schlimmster XSS-Fehler, und die meisten Anwendungen haben irgendwann einen.

Die übliche Alternative, ein HttpOnly-Cookie, entfernt den Skriptzugriff ganz — eine XSS-Nutzlast kann das Token nicht mehr lesen und abziehen —, aber der Browser hängt das Cookie automatisch an, was die Cross-Site-Anfragenfälschung zur Bedrohung macht, der man mit SameSite-Attributen und CSRF-Tokens begegnet. Tokens nur im Arbeitsspeicher zu halten, umgeht beide Probleme und verliert die Sitzung bei jedem Neuladen. Es gibt keine universell richtige Antwort, und das Gegenteil vorzutäuschen ist der Weg, auf dem Teams am Ende das Schlechteste aus jeder Option bekommen. Was Speicher zum Leck macht, ist die Kombination, die niemand absichtlich gewählt hat: skriptlesbarer Speicher, lange Lebensdauern, breite Scopes und keine Rotation der Refresh-Tokens, die den Wert einer Kopie verkleinern würde. Wählen Sie den Kompromiss bewusst, halten Sie Zugriffstokens so oder so kurzlebig, und schreiben Sie die Begründung dorthin, wo der nächste Entwickler sie findet.

Quellen und weiterführende Lektüre

Häufig gestellte Fragen

Sind die Daten in einem JWT verschlüsselt?

Nein. In einem standardmäßig signierten JWT sind Header und Payload Base64url-kodiert, was jeder dekodieren kann — öffnen Sie ein Token im JWT-Decoder, und jeder Claim ist im Klartext lesbar. Die Signatur liefert Integrität und Herkunftsnachweis, keine Vertraulichkeit. Verschlüsselte JWTs (JWE) existieren, sind in der Praxis aber die Ausnahme. Behandeln Sie jeden Claim als öffentlich für jeden, der das Token besitzt, und für jedes Protokoll, jeden Proxy und jeden Screenshot, den das Token durchquert.

Wir verifizieren die Signatur. Warum ist das nicht genug?

Weil eine gültige Signatur nur beweist, dass das Token vom Schlüsselinhaber ausgestellt und seitdem nicht verändert wurde. Sie sagt nichts darüber, ob das Token abgelaufen ist (exp), vom Aussteller stammt, dem Sie vertrauen (iss), oder für Ihren Dienst statt für einen anderen mit demselben Schlüssel ausgestellt wurde (aud) — und sie sagt nichts, wenn Ihr Verifizierer das Token den Algorithmus wählen oder den Schlüsselort liefern ließ. Die Signaturprüfung ist die erste Hälfte der Validierung. Die Claim-Checkliste, mit dem Algorithmus auf Ihrer Seite festgenagelt, ist die zweite Hälfte, und Lecks wohnen in der Lücke dazwischen.

Sollten Tokens im localStorage oder in einem Cookie leben?

Jede Option tauscht einen Angriff gegen einen anderen. localStorage ist für jedes Skript auf der Seite lesbar, also kann ein einziger XSS-Fehler oder ein bösartiges Drittanbieter-Skript jedes gespeicherte Token kopieren. Ein HttpOnly-Cookie kann von Skripten überhaupt nicht gelesen werden, wird aber automatisch mitgesendet, was CSRF zur Bedrohung macht, die man mit SameSite und CSRF-Tokens handhabt. Beides kann mit kurzen Lebensdauern der Zugriffstokens und Rotation der Refresh-Tokens vertretbar sein. Die unvertretbare Option ist, standardmäßig zu wählen, lange Lebensdauern zu verwenden und die Entscheidung nie zu überdenken.