Moderne Web-Anwendungen sehen anders aus als klassische Form-basierte Apps: Single-Page-Apps mit Fetch/Axios, JSON-APIs statt HTML-Form-Submissions, Bearer-Tokens in Authorization-Headers statt Cookies. Die CSRF-Lage ist dort fundamental anders — manche Setups sind strukturell CSRF-frei, andere haben unerwartete Risiken, die man bei reiner Form-Sicht nicht sieht. Dieser Artikel räumt mit dem CORS-Mythos auf, zeigt die strukturellen Eigenschaften und ordnet die Edge-Cases.
Cookie-Auth vs. Bearer-Token-Auth — der zentrale Unterschied
Die wichtigste Frage für CSRF in modernen Apps: Wie wird authentifiziert?
Cookie-basierte Auth (klassisch):
- Session-Cookie wird vom Browser automatisch bei jeder Anfrage an die Origin mitgesendet.
- Auch bei Cross-Site-Anfragen (vor SameSite) — genau das ermöglicht CSRF.
- Setup: Server setzt
Set-Cookie: session=...; Browser merkt sich; bei jedem Request automatisch dabei.
Bearer-Token-Auth (moderne SPA):
- Token wird im Authorization-Header geschickt —
Authorization: Bearer <token>. - Browser sendet diesen Header nicht automatisch — er muss aktiv von JavaScript gesetzt werden.
- Cross-Site-JavaScript kann den Token nicht lesen (Same-Origin-Policy gegen
localStorage/sessionStorage). - Setup: Token wird im
localStorage/sessionStorageoder in einem JS-Variable gehalten; bei jedem Fetch wird Header explizit gesetzt.
Konsequenz:
- Cookie-Auth: CSRF-relevant. Schutz durch SameSite, Token, Origin-Check nötig.
- Bearer-Token-Auth: strukturell CSRF-frei. Browser sendet Token nicht automatisch cross-site. Eine Angreifer-Seite kann keinen Authorization-Header mit deinem Token setzen, weil sie ihn nicht lesen kann.
Das ist der fundamentale Unterschied: Wer Bearer-Tokens nutzt, hat CSRF strukturell gelöst. Das ist einer der wenigen wirklichen Sicherheits-Vorteile von Token-Auth gegenüber Cookie-Auth.
Der CORS-Mythos
Ein häufiges Missverständnis: „Wir haben CORS strict konfiguriert (Access-Control-Allow-Origin: nur unsere Domain), also sind wir CSRF-sicher."
Das ist falsch. CORS und CSRF sind verschiedene Probleme mit verschiedenen Schutz-Mechanismen.
Was CORS macht:
- Erlaubt JavaScript auf einer Origin, die Antwort einer Anfrage an eine andere Origin zu lesen.
- Ohne CORS verbietet Same-Origin-Policy das Lesen der Response.
- Strict-CORS heißt: nur erlaubte Origins können die Antwort lesen.
Was CORS nicht macht:
- Verhindert nicht, dass die Anfrage gestellt wird.
- Verhindert nicht, dass der Server die Aktion ausführt.
Konkretes Beispiel:
evil.example sendet:
// Auf evil.example
fetch('https://bank.example/api/transfer', {
method: 'POST',
credentials: 'include', // schickt Cookies mit (Same-Site=None)
body: 'to=attacker&amount=10000',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});Was passiert:
- Browser schickt die POST-Anfrage an
bank.example. - Cookies werden mitgeschickt (wenn
SameSite=None). - Bank-Server führt die Aktion aus — Überweisung läuft.
- Bank-Server antwortet mit „OK" oder Fehler.
- CORS prüft: darf
evil.exampledie Antwort lesen? - CORS lehnt ab (Allow-Origin ist nur
bank.example). - JavaScript auf evil.example kann die Antwort nicht lesen — aber das Geld ist schon weg.
CORS hat verhindert, dass der Angreifer die Antwort sieht. Es hat nicht verhindert, dass die Aktion stattfindet.
Konsequenz: CORS ist kein CSRF-Schutz. CSRF-Schutz braucht eine der echten Schichten: SameSite-Cookies, CSRF-Token, Origin-Check, Custom-Header.
CORS-Preflight als unbeabsichtigter CSRF-Schutz
Trotzdem gibt es eine Konstellation, in der CORS indirekt CSRF schützt: bei Pre-Flight-Anfragen.
Browser löst eine OPTIONS-Pre-Flight-Anfrage aus, wenn:
- HTTP-Methode nicht GET / HEAD / POST ist (also PUT, PATCH, DELETE).
- ODER: Content-Type ist nicht einer von
application/x-www-form-urlencoded,multipart/form-data,text/plain. - ODER: Custom-Header werden gesetzt.
Wenn der Server in der OPTIONS-Antwort den Cross-Site-Zugriff ablehnt (kein Access-Control-Allow-Origin: evil.example), schickt der Browser die eigentliche Anfrage gar nicht erst.
Praktische Konsequenz:
- JSON-API mit
Content-Type: application/jsonlöst Pre-Flight aus. Wenn CORS strict konfiguriert ist, fängt der Browser den CSRF-Versuch ab. - JSON-API mit
Content-Type: text/plain(manche Angreifer-Tricks) löst KEINEN Pre-Flight aus. Anfrage geht durch. - GET-Anfragen lösen nie Pre-Flight aus. CSRF über GET bleibt offen.
Das macht strict-CORS zu einem partiellen CSRF-Schutz für JSON-APIs — aber nicht universell. Defense-in-Depth verlangt zusätzliche Schichten.
Custom-Header als Anti-CSRF
Eine elegante Variante für JSON-APIs: Eine API verlangt einen Custom-Header, den Browser nicht automatisch bei Cross-Site-Anfragen sendet.
// SPA setzt explizit Custom-Header
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ to: '...', amount: 100 }),
});
// Server verlangt diesen Header
app.use((req, res, next) => {
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).send('Missing custom header');
}
next();
});Warum das funktioniert:
- Eine Cross-Origin-Anfrage mit Custom-Header löst Pre-Flight aus.
- Pre-Flight scheitert, wenn CORS den Custom-Header nicht erlaubt.
- Eine Angreifer-Seite, die keinen Pre-Flight bestanden hat, kann den Custom-Header nicht setzen.
Voraussetzung: Server muss strikt den Custom-Header verlangen — fehlt er, ablehnen.
Vorteil gegenüber CSRF-Token:
- Kein Server-State (kein Token-Storage).
- Kein Template-Rendering von Token-Werten.
- Stateless, skaliert.
Wofür dieser Trick funktioniert:
- JSON-APIs mit
Content-Type: application/json(löst Pre-Flight aus). - APIs mit anderen non-simple Content-Types.
Wofür er NICHT funktioniert:
- Form-Submits mit
application/x-www-form-urlencoded— kein Pre-Flight. - GET-Anfragen — kein Pre-Flight, Custom-Header werden nicht automatisch geschickt, aber GET ist eh ohne Side-Effects auszulegen.
X-Requested-With als historischer Klassiker:
Der X-Requested-With: XMLHttpRequest-Header wurde traditionell von jQuery und anderen AJAX-Libraries automatisch gesetzt. Viele Sites prüften ihn als Anti-CSRF. Heute ist er etwas aus der Mode — moderner Code setzt eigene App-spezifische Header.
SPA mit Cookie-Auth — der häufige Hybrid-Fall
Eine sehr verbreitete Konstellation: SPA-Frontend + Cookie-basierte Auth (statt Bearer-Token).
Warum diese Wahl?
- HttpOnly-Cookies sind XSS-resistent — JavaScript kann sie nicht lesen (im Gegensatz zu localStorage-Token).
- Browser-Auto-Refresh — Tab schließen und öffnen, Session bleibt.
- Einfacher Server-Side-Logout — Cookie invalidieren.
Aber: Cookie-Auth ist CSRF-anfällig. SPA-Architektur ändert das nicht.
Pragmatische Lösung:
- Auth-Cookie mit
HttpOnly,Secure,SameSite=Lax,__Host--Präfix (siehe samesite-cookies). - CSRF-Token zusätzlich — entweder Synchronizer- oder Double-Submit-Pattern (siehe csrf-tokens-und-double-submit).
- Origin-Header-Check als Defense-in-Depth.
- Custom-Header-Anforderung als Anti-CSRF-Bonus.
// SPA-Init: CSRF-Token vom Server abholen
async function getCsrfToken() {
const res = await fetch('/api/csrf-token', { credentials: 'include' });
const { token } = await res.json();
return token;
}
const csrfToken = await getCsrfToken();
// Bei jedem state-changing Request
await fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ to, amount }),
});Token im Header statt im Body ist in SPAs Standard — kein Form-Field-Equivalent.
GraphQL-Endpunkte und CSRF
GraphQL-Endpunkte sind eine eigene Klasse:
Typische Eigenheiten:
- POST mit
Content-Type: application/json— löst Pre-Flight aus, CORS-Pre-Flight wirkt indirekt. - Single Endpoint (
/graphql) für alle Queries und Mutations. - Cookie-Auth oder Bearer-Token-Auth verbreitet — je nach Setup.
CSRF-Risiken:
Wenn der GraphQL-Server GET-Queries akzeptiert (manche tun das für Query-Caching), und Cookie-Auth aktiv ist, ist CSRF über GET möglich:
<!-- Angreifer-Seite -->
<img src="https://api.example/graphql?query=mutation+%7B+deleteAccount+%7D">Wenn der Server GET-Mutations akzeptiert, wird die Mutation ausgeführt — klassisches CSRF.
Schutz:
- GET-Queries nur für read-only-Operations zulassen, niemals Mutations.
- Apollo Server hat seit v3 standardmäßig CSRF Prevention aktiviert — Mutations verlangen
Content-Type: application/jsonund einen Custom-Header. - GraphQL-Yoga, Mercurius und andere haben ähnliche Defaults.
Plus Anti-Pattern vermeiden: GraphQL über application/x-www-form-urlencoded zulassen — Pre-Flight greift dann nicht, CSRF wird möglich.
Mobile-App-Backends
Eine oft vergessene Edge-Case: Mobile-Apps rufen Backend-APIs auf — und der Browser-CSRF-Mechanismus gilt dort nicht.
Typische Mobile-Auth:
- Bearer-Token in Authorization-Header (JWT, OAuth-Token).
- Token im Keystore (iOS Keychain, Android Keystore).
- Keine Cookies — Mobile-Apps sind keine Browser.
CSRF-Status: Mobile-Apps haben kein klassisches CSRF — kein automatisches Cookie-Mitsenden, keine Cross-Site-Requests im Browser-Sinn.
Aber:
- WebView-Komponenten in Hybrid-Apps (Cordova, Capacitor, native WebView): hier gelten Browser-Regeln. CSRF-Risiken zurück.
- App-Web-Auth-Flows (OAuth via Browser-Tab): siehe csrf-in-oauth-und-saml.
- Universal Links / Custom Schemes mit Deep-Link-Auth: eigene Risiko-Klasse.
Konsequenz: Mobile-Backend-APIs brauchen typisch keinen CSRF-Schutz, wenn pur Bearer-Token-Auth. Sobald aber Cookie-Auth oder Web-Auth-Flows ins Spiel kommen, gelten die normalen CSRF-Regeln.
Pragmatische Empfehlungs-Matrix
| Setup | CSRF-Risiko | Schutz-Empfehlung |
|---|---|---|
| SPA + Bearer-Token (Authorization-Header) | Keins (strukturell) | Kein expliziter CSRF-Schutz nötig; Bearer-Token-Sicherheit ist eigenes Thema |
SPA + Cookie-Auth, JSON-API mit Content-Type: application/json | Mittel (Pre-Flight schützt teils) | CSRF-Token + Origin-Check |
| SPA + Cookie-Auth, Form-Submit | Hoch | CSRF-Token (Pflicht) + SameSite + Origin-Check |
| Klassische Form-basierte App | Hoch | CSRF-Token (Framework-Default) + SameSite |
| GraphQL-Endpunkt mit Cookie-Auth | Hoch (wenn GET-Mutations) | Apollo CSRF-Prevention aktivieren; nur POST für Mutations |
| Mobile-App-Backend (pure Bearer-Token) | Keins | Token-Auth-Sicherheit fokussieren |
| OAuth-Authorization-Flow | Eigenständig | state-Parameter Pflicht (siehe Kap 12 OAuth-Artikel) |
| WebView in Hybrid-App | Wie Browser | Browser-CSRF-Regeln |
Häufige Stolperfallen
LocalStorage-Token + XSS = neues Risiko
Bearer-Tokens im localStorage sind nicht XSS-resistent — wenn deine App XSS hat, kann das Skript den Token auslesen und an Angreifer schicken. HttpOnly-Cookies sind XSS-resistenter, aber CSRF-anfällig. Die Wahl zwischen den beiden ist ein Trade-off zwischen XSS- und CSRF-Risiko. Defense-in-Depth: beides ernst nehmen.
CORS mit credentials: include — gefährlich falsch konfiguriert
Wenn der Server Access-Control-Allow-Origin: * UND Access-Control-Allow-Credentials: true setzen würde, wäre das ein massiver Sicherheits-Bug — der Browser lehnt diese Kombination zum Glück ab. Aber: dynamische Origin-Reflektion (Origin aus Request kopieren) + Credentials = CSRF-Tor. Allow-List statt Reflektion.
GET-Queries in GraphQL sind oft ein Anti-Pattern
Manche GraphQL-Setups akzeptieren Queries per GET für CDN-Caching. Wenn aber Mutations auch per GET erlaubt sind, ist CSRF-Tor offen. Apollo Server v3+ erzwingt POST für Mutations und einen Custom-Header — Default zustimmen, nicht deaktivieren.
Content-Type: text/plain als Bypass
Ein Angreifer kann eine POST-Anfrage mit Content-Type: text/plain stellen (zählt als „simple request", kein Pre-Flight) — und JSON in den Body packen. Wenn dein Server den Body trotzdem als JSON parst, hat er soeben CSRF-Schutz umgangen. Server sollte strikt nur den erwarteten Content-Type akzeptieren.
Apollo CSRF Prevention seit v3
Apollo Router/Server hat seit Version 3 eingebaute CSRF-Prevention. Es verlangt entweder eine Pre-Flight-auslösende Eigenschaft (Content-Type, Custom-Header) oder lehnt die Anfrage ab. Default aktiv — wer es deaktiviert, sollte einen sehr guten Grund haben.
Bearer-Token-Refresh-Workflow als Edge-Case
Refresh-Tokens werden oft als HttpOnly-Cookie gespeichert, während Access-Tokens im localStorage oder Memory liegen. Damit hat man Cookie-Auth für den Refresh-Flow — und der ist CSRF-anfällig. Lösung: SameSite=Strict für Refresh-Cookie + Custom-Header für Refresh-Endpunkt.
WebSockets und CSRF
WebSocket-Connections starten mit einem HTTP-Handshake — Cookies werden mitgeschickt. Eine fremde Origin kann eine WebSocket-Verbindung mit deinen Cookies öffnen. Origin-Header-Check beim Handshake ist die Standard-Verteidigung; viele Libraries machen das nicht per Default. Bei sensitiven WebSocket-Endpunkten explizit prüfen.
Weiterführende Ressourcen
Externe Quellen
- OWASP CSRF Cheat Sheet — Custom Headers
- MDN — CORS
- Apollo Router — CSRF Prevention
- PortSwigger — Bypassing CSRF Token Validation
- PortSwigger — CORS Vulnerabilities
- Angular — Modern CSRF Protection
- OWASP — CSRF Community Page