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.

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 / sessionStorage oder 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:

JavaScript cors-doesnt-stop-csrf.js
// 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:

  1. Browser schickt die POST-Anfrage an bank.example.
  2. Cookies werden mitgeschickt (wenn SameSite=None).
  3. Bank-Server führt die Aktion aus — Überweisung läuft.
  4. Bank-Server antwortet mit „OK" oder Fehler.
  5. CORS prüft: darf evil.example die Antwort lesen?
  6. CORS lehnt ab (Allow-Origin ist nur bank.example).
  7. 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/json lö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.

JavaScript custom-header-anti-csrf.js
// 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.

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.
JavaScript spa-cookie-csrf-flow.js
// 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:

HTML graphql-csrf-via-get.html
<!-- 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/json und 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

SetupCSRF-RisikoSchutz-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/jsonMittel (Pre-Flight schützt teils)CSRF-Token + Origin-Check
SPA + Cookie-Auth, Form-SubmitHochCSRF-Token (Pflicht) + SameSite + Origin-Check
Klassische Form-basierte AppHochCSRF-Token (Framework-Default) + SameSite
GraphQL-Endpunkt mit Cookie-AuthHoch (wenn GET-Mutations)Apollo CSRF-Prevention aktivieren; nur POST für Mutations
Mobile-App-Backend (pure Bearer-Token)KeinsToken-Auth-Sicherheit fokussieren
OAuth-Authorization-FlowEigenständigstate-Parameter Pflicht (siehe Kap 12 OAuth-Artikel)
WebView in Hybrid-AppWie BrowserBrowser-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

/ Weiter

Zurück zu CSRF & SameSite

Zur Übersicht