Klartext und Tresor: warum dein localStorage offen liegt und dein Login nicht
Im letzten Teil endete der Scan mit einer Beobachtung, die sich verkehrt anfühlt. Die heiklen Tokens lagen offen im localStorage, leicht zu finden. Die ganz großen Web-Logins, Google und Microsoft, tauchten gar nicht auf. Nicht weil diese Dienste keine Tokens hätten, im Gegenteil, sie verwalten einige der wertvollsten Sitzungen, die ein Browser überhaupt kennt. Sie tauchten nicht auf, weil ihre Tokens an einem Ort liegen, an den der Scanner nicht herankam.
Die naheliegende Annahme ist, dass alle Tokens irgendwo im Browser herumliegen und ein gründliches Werkzeug sie alle findet. Das stimmt nicht. Nicht aller Token-Speicher ist gleich gebaut. Das eine liegt im Klartext auf der Platte, das andere in einem Tresor, den der Browser für sich selbst eingerichtet hat. Diese eine Asymmetrie erklärt beides: warum der Scan genau das fand, was er fand, und warum der naheliegende Fix, „dann verschlüssele das Cookie eben besser“, am Ende am Problem vorbeigeht.
Die ersten beiden Teile dieser Reihe bauen die Grundlage. Teil 1 zeigte, dass ein JWT ein Format ist, kein Login-System, und dass sein wundester Punkt im Browser der Bearer-Charakter ist: wer das Token hat, ist drin. Artikel A ging mit einem selbstgebauten Scanner durch den eigenen Browser und sortierte Müll von Risiko. Dieser Teil löst auf, was dort offen blieb, und schließt die Reihe.
Klartext gegen Tresor
localStorage liegt auf der Platte im Klartext. Das war der Befund aus Artikel A, und er ist der Grund, warum der Scanner überhaupt etwas fand. Er las die Profildateien als Bytes, ohne Entschlüsselung, ohne Admin-Rechte, im ganz normalen Nutzer-Kontext. Was im localStorage steht, steht dort lesbar, für den Browser ist es nur ein String.
Cookies sind anders gebaut. Seit Chrome 127 (Juli 2024) steckt der Cookie-Speicher hinter App-Bound Encryption, davor lag er hinter dem älteren DPAPI-Verfahren. Der Schlüssel, mit dem die Cookies verschlüsselt sind, ist so verpackt, dass nur die signierte Browser-Programmdatei ihn wieder aufmachen kann, und zwar über einen Systemdienst, der vor dem Entschlüsseln prüft, wer ihn da überhaupt fragt. Ein fremder Prozess, der einfach nur die Cookie-Datei kopiert, hält danach verschlüsselten Müll in der Hand. Er hat zwar die Bytes, aber nicht den Schlüssel, und an den Schlüssel kommt er nicht, ohne den Dienst zu überzeugen, dass er der Browser sei.
Das ist der ganze Unterschied im Kern. Beim localStorage genügt es, die Datei zu lesen, dann hat man den Inhalt. Beim Cookie hat man nach dem Lesen erst die halbe Miete, weil der Inhalt verschlüsselt ist und der Schlüssel an einer Tür hängt, die nach Identität fragt statt nach Rechten. Der Aufwand für das Eine und das Andere liegt um eine ganze Größenordnung auseinander, und diese Größenordnung ist der Grund, warum die großen Logins im Scan einfach fehlten.
Genau hier liegen Googles und Microsofts Sitzungen. Ihre Logins laufen über HttpOnly-Cookies, und die haben damit zwei Schutzschichten übereinander. Erstens sind sie für JavaScript unsichtbar, eingeschleustes Skript kann sie nicht lesen, das war der Same-Origin-Punkt aus Artikel A. Zweitens sind sie auf der Platte verschlüsselt, ein Datei-lesender Prozess kommt nicht an den Klartext. localStorage bekommt keine dieser beiden Schichten. Es ist für jedes Skript der Origin lesbar und liegt obendrein unverschlüsselt auf der Platte.
Die Abwesenheit der großen Logins im Scan ist damit keine Lücke im Werkzeug, sondern selbst der Beweis. Du kannst das ohne mein Tool nachstellen. Öffne bei ChatGPT die DevTools und schau in den localStorage, du findest sein Token dort im Klartext. Mach dasselbe bei einem Google-Dienst, und im localStorage liegt nichts Brauchbares, der Login steckt in einem HttpOnly-Cookie, dessen Wert dir die Konsole gar nicht erst zeigt. Das gefährlich Exponierte ist sichtbar, das gut Geschützte entzieht sich. Genau diese Schieflage findet der Scanner, und sie kehrt die Intuition um: Der Speicher, der am leichtesten auszulesen ist, hält ausgerechnet die Tokens, die am ehesten ein Problem sind, während die wertvollsten Sitzungen dort liegen, wo der Byte-Scanner nichts holt.
Ein Infostealer mit guten Absichten
Mein Audit-Tool und ein Infostealer lesen dieselben Bytes auf dieselbe Weise. Beide gehen die Profildateien durch, beide ziehen heraus, was nach Token aussieht. Der einzige Unterschied liegt darin, wohin das Gelesene geht: bei mir auf meinen eigenen Bildschirm, beim Stealer über die Leitung zu einem fremden Server. Die Technik des Lesens ist identisch.
Das ist kein Makel des Tools. Genau so sieht das Bedrohungsmodell aus. Wenn ein harmloses Audit-Skript ohne besondere Rechte den localStorage ausräumen kann, dann kann es Schad-Software, die als derselbe Nutzer läuft, genauso. Und genau als dieser Nutzer läuft sie, denn ein Infostealer, den du dir einfängst, startet in deinem eigenen Konto, nicht in einem fremden.
Vor Chrome 127 galt das sogar für Cookies. DPAPI band den Schlüssel allein an den Windows-Account, also durfte jeder Prozess, der als du läuft, ihn benutzen und entschlüsseln. Für einen Datei-kopierenden Stealer war das Cookie damit so offen wie der localStorage. App-Bound Encryption hat diese Grenze verschoben, weg von der Frage „bist du dieser Nutzer?“ hin zu „bist du der signierte Browser?“. Als derselbe Nutzer zu laufen reicht seitdem nicht mehr, um an den Cookie-Schlüssel zu kommen.
Deshalb läuft ein simpler, Datei-kopierender Stealer bei den Cookies der großen Anbieter heute gegen eine Wand. Beim localStorage läuft er nicht gegen eine Wand, weil dort nie eine stand. Der localStorage war nie verschlüsselt, und keine Browser-Version hat daran etwas geändert. Die Asymmetrie aus dem letzten Abschnitt ist also nicht Zufall, sondern das direkte Resultat davon, dass die eine Speicherart einen Tresor bekam und die andere nie.
„SYSTEM = geknackt“ stimmt nicht
Die naheliegende Annahme an dieser Stelle ist: Wenn der Cookie-Schlüssel an einen SYSTEM-Dienst hängt, dann starte ich meinen Prozess eben selbst als SYSTEM. Werkzeuge dafür gibt es, PsExec etwa. Wer als SYSTEM läuft, hat auf einer Windows-Maschine die höchsten Rechte, also ist die Verschlüsselung damit umgangen. Stimmt nicht, und der Grund ist, was App-Bound Encryption gerade verschoben hat.
Die Schutzgrenze liegt nicht mehr beim Privileg, sondern bei der Identität der Programmdatei. Der Systemdienst fragt nicht „bist du privilegiert genug?“, sondern „bist du die echte, signierte Browser-Programmdatei am erwarteten Pfad?”. SYSTEM-Rechte beantworten diese zweite Frage nicht. Ein als SYSTEM gestarteter Fremdprozess ist immer noch nicht der Browser, und der Dienst gibt ihm den Schlüssel nicht.
Die Bypässe, die in der Praxis funktionieren, schlagen deshalb nicht SYSTEM, sie geben sich als der Browser aus. CyberArks „C4 Bomb“ und Verfahren per Prozess-Injektion zielen genau darauf: nicht mehr Privileg, sondern die Identität des Browsers vortäuschen oder sich in den laufenden Browser-Prozess hineinschmuggeln. Wie das im Detail geht, schreibe ich hier nicht aus.
App-Bound Encryption ist trotzdem kein undurchdringliches Schild. Jede Verschlüsselung auf der Platte ist gegen einen Angreifer, der schon als du auf der laufenden Maschine sitzt, prinzipiell überwindbar. Der Browser muss seinen Cookie-Speicher ohne dein Zutun aufmachen können, sonst müsstest du bei jedem Start ein Passwort eingeben. Was der Browser von selbst entschlüsseln kann, kann auch Malware, die als du läuft, ihn entschlüsseln lassen, indem sie sich für ihn ausgibt. App-Bound Encryption ist Reibung, nicht Magie. Sie hebt die Latte gegen den Massen-Infostealer, der einfach nur Dateien kopiert, sie schließt nichts unwiderruflich ab.
Trotzdem greift die pauschale Abtuung „liegt auf deiner Platte, also sowieso verloren“ zu kurz. Es gibt einen echten Unterschied zwischen „liest eine Klartext-Datei“ und „muss sich als der signierte Browser-Prozess ausgeben oder sich in ihn injizieren“. Das Erste kann jedes Wald-und-Wiesen-Skript, das Zweite kostet Aufwand und fliegt eher auf. Dass die Schicht real wirkt, zeigt ein Nebeneffekt: legitime Werkzeuge wie yt-dlp, die Cookies aus dem Browser exportieren, um sie weiterzuverwenden, brachen auf Windows-Chrome genau wegen App-Bound Encryption (Beleg). Wenn die Schicht sogar Tools im selben Netz fängt, die explizit für den Cookie-Export gebaut sind, dann ist sie keine reine Behauptung.
Bindung statt Verschlüsselung
Das eigentliche Versagen war nie, dass das Cookie zu schwach verschlüsselt war. Es ist, dass ein Bearer-Token überhaupt kopierbar ist. „Wer es hat, ist drin“ heißt eben auch: wer eine Kopie hat, ist genauso drin. Verschlüsselung auf der Platte ändert daran nichts Grundsätzliches, sie macht das Beschaffen der Kopie nur teurer. Ist das Token einmal heraus, funktioniert es überall.
Der Fix, der eine gestohlene Kopie tatsächlich wertlos macht, setzt eine Schicht tiefer an. Er bindet das Token an das Gerät, auf dem es entstanden ist. Eine Kopie ohne dieses Gerät ist dann kein gültiger Schlüssel mehr, egal wie sauber sie kopiert wurde. Der Trick dahinter ist immer derselbe: Statt ein Geheimnis mitzuschicken, das jeder weiterreichen kann, der es einmal gesehen hat, beweist der Browser bei jeder Sitzung neu, dass er ein Geheimnis besitzt, das er nie herausgibt. Was über die Leitung geht, ist nur der Beweis, nicht das Geheimnis selbst. Eine abgefangene Kopie des Beweises hilft nicht weiter, weil der nächste Beweis schon ein anderer ist und sich ohne das Geheimnis nicht erzeugen lässt. Es gibt zwei Formen davon, eine in der Hardware des Browsers und eine eine Ebene höher in der Anwendung.
- DBSC (Device Bound Session Credentials). Der Browser legt beim Login einen privaten Schlüssel in einem Sicherheitschip des Geräts ab, dem TPM, aus dem er sich nicht herauskopieren lässt. Mit diesem Schlüssel signiert er alle paar Minuten eine Anfrage des Servers und beweist so, dass die Sitzung noch auf demselben Gerät läuft, ganz ohne Zutun des Nutzers. Ein Cookie, das jemand auf einen anderen Rechner kopiert, ist dort tot, weil der private Schlüssel fehlt und das fremde Gerät die Anfrage des Servers nicht beantworten kann. Laut Google ist DBSC seit Chrome 146 auf Windows allgemein verfügbar. Eine Variante für macOS über dessen Secure Enclave ist angekündigt, Edges Origin-Trial endete ohne allgemeine Verfügbarkeit.
- DPoP (RFC 9449). Dasselbe Prinzip, aber auf der Ebene der Anwendung statt in der Hardware. Der Client beweist bei jedem token-geschützten Request, dass er ein bestimmtes Schlüsselpaar besitzt. Ein gestohlenes Token ohne den dazugehörigen privaten Schlüssel ist nutzlos, weil der Dieb den Besitz nicht nachweisen kann. Es ist derselbe Besitznachweis wie bei DBSC, nur in der OAuth-Schicht angesiedelt und nicht im Browser-Unterbau.
DBSC stoppt allerdings nicht den lokalen Angreifer, der schon auf deinem Gerät sitzt und die laufende Sitzung direkt benutzt. Wer als du auf der Maschine ist, kann den Browser bedienen, und der Browser hat sein gebundenes Token griffbereit. Was DBSC stoppt, ist die Wiederverwendung anderswo: dass die gestohlene Kopie auf dem Server des Angreifers noch funktioniert. Es schließt damit genau den Weg, den der Infostealer aus Artikel A geht, nämlich kopieren und vom eigenen Server aus benutzen. Das ist nicht das ganze Problem, aber es ist der Teil, der die Massen-Diebstähle aus den hunderttausend ausgelesenen Browsern überhaupt erst lukrativ macht.
Bearer raus, gebunden rein
Drei Teile, eine Linie. Teil 1: Ein JWT ist nur ein Format, die Gefahr ist das Bearer-Token im localStorage, das du nicht widerrufen kannst. Artikel A: Schau selbst nach, das meiste, was da liegt, ist Müll, das Gefährliche ist das Bearer-Token im Klartext, und die sicheren Logins sind im Scan unsichtbar. Dieser Teil: Sie sind unsichtbar, weil sie im Tresor liegen, und der echte Fix ist kein besserer Tresor, sondern ein Token, das an das Gerät gebunden ist.
Wo du das Token speicherst, im localStorage, im Cookie oder im TPM, war die ganze Zeit über die nachgelagerte Frage. Die vorgelagerte ist, ob das Token überhaupt eine reine Trägerkarte ist, die in jeder fremden Hand funktioniert, oder an etwas hängt, das sich nicht mitkopieren lässt. Eine bessere Schublade für die Trägerkarte zu bauen, war immer nur ein Aufschub. Solange die Karte selbst überall funktioniert, wo eine Kopie von ihr auftaucht, bleibt der Diebstahl lohnend, und der ganze Aufwand für den Tresor erschwert ihn nur, statt ihn zu entwerten.
Die Hebel sind deshalb dieselben drei, um die sich der Streit schon im ersten Teil drehte: das Token ans Gerät binden, ihm eine kurze Lebensdauer geben, es widerrufbar machen. Alle drei zielen nicht darauf, das Token besser zu verstecken, sondern darauf, eine gestohlene Kopie schnell oder sofort nutzlos werden zu lassen. Bearer raus, gebunden rein.
Dies ist der dritte und letzte Teil der Reihe. Er baut auf Teil 1: „Ein JWT ist ein Format, kein Login-System“ und Artikel A: „Schau in deinen Browser“ auf.