OAuth- und SAML-Flows sind ihre eigene CSRF-Klasse. Anders als klassisches CSRF, wo der Angreifer Aktionen im Namen des Opfers auslöst, gibt es hier eine charakteristische Variante: der Angreifer erzwingt eine Anmeldung des Opfers in einen Angreifer-kontrollierten Account — oder einen Account-Link, bei dem der Angreifer mit der Identität des Opfers über die OAuth-Brücke laufen kann. Dieser Artikel zeigt das Konzept, den state-Parameter und PKCE als Standard-Schutz, und welche bekannten Bug-Bounty-Reports die Klasse dokumentieren.

Wie der OAuth-Authorization-Code-Flow läuft

Zum Verständnis des CSRF-Risikos zuerst der normale Ablauf:

Akteure:

  • Client — deine App (z. B. app.example).
  • Authorization Server (AS) — der Identity-Provider (z. B. Google, GitHub).
  • Resource Owner — die Nutzer:in.

Standard-Flow:

Plain oauth-flow.txt
1. User klickt "Mit Google anmelden" in deiner App
2. App redirected User zu Google:
   https://google.com/oauth2/authorize?
     client_id=...&
     redirect_uri=https://app.example/callback&
     response_type=code&
     scope=openid+email
3. User loggt sich bei Google ein, authorisiert die App
4. Google redirected zurück zu app.example/callback?code=xyz
5. App-Backend tauscht code gegen access_token
6. App ist jetzt authentifiziert für User

Wo CSRF reinkommt:

Schritt 4 ist eine Top-Level-Redirect-Anfrage an deine Callback-URL — vom Browser des Opfers, mit dem Authorization-Code im Query-String. Wenn ein Angreifer dem Opfer einen Link unterschiebt, der zu app.example/callback?code=AngreiferCode führt — und dein Backend den Code einlöst, verknüpft es das Opfer-Konto mit dem Angreifer-Account beim Identity-Provider.

Der OAuth-CSRF-Angriff im Detail

Szenario 1 — Account-Link-Hijacking:

Das Opfer hat ein App-Konto und will optional „Google verbinden". Der Angreifer:

  1. Startet selbst den OAuth-Flow mit dem Google-Angreifer-Account.
  2. Stoppt bei Schritt 4 — bekommt seine Callback-URL: app.example/callback?code=angreiferCode.
  3. Schickt diesen Link an das Opfer (per Phishing-Mail oder gefährliche Webseite).
  4. Opfer ist in seiner App eingeloggt; klickt auf den Link.
  5. App-Backend tauscht den Code beim Identity-Provider ein — bekommt Tokens für den Angreifer-Google-Konto.
  6. App-Backend verknüpft das Angreifer-Google-Konto mit dem Opfer-App-Konto.
  7. Ab jetzt kann der Angreifer per „Mit Google anmelden" in das Opfer-App-Konto.

Sehr böse: der Angreifer kennt das Opfer-App-Passwort nicht, hat sich aber als „Google-Identität" daneben gehängt.

Szenario 2 — Login-CSRF (Force-Login):

Variante: Angreifer zwingt das Opfer, sich in einen Angreifer-kontrollierten Account einzuloggen. Folge: alle Suchen, Daten-Eingaben, Käufe des Opfers landen im Angreifer-Konto.

Diese zweite Variante ist weniger destruktiv, aber Privacy-relevant — vor allem bei Suchmaschinen, Online-Shops, Cloud-Diensten.

Der state-Parameter als CSRF-Schutz

Die Standard-Antwort: der state-Parameter im OAuth-Flow.

Wie es funktioniert:

  1. Beim Start des Flows generiert deine App ein zufälliges, unvorhersehbares Token und speichert es serverseitig (an die Session gebunden).
  2. Dieses Token wird als state=<wert> mit der Authorize-Redirect mitgegeben.
  3. Identity-Provider gibt den state-Wert unverändert im Callback zurück.
  4. Dein Callback-Handler prüft: stimmt der state-Wert mit dem in der Session?

Konkret:

JavaScript oauth-state.js
import crypto from 'crypto';

// 1. Start des Flows
app.get('/auth/google', (req, res) => {
  const state = crypto.randomBytes(32).toString('hex');
  req.session.oauth_state = state;

  const url = new URL('https://google.com/oauth2/authorize');
  url.search = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: 'https://app.example/auth/google/callback',
    response_type: 'code',
    scope: 'openid email',
    state,
  });
  res.redirect(url);
});

// 2. Callback
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;
  if (!state || state !== req.session.oauth_state) {
    return res.status(403).send('CSRF: invalid state');
  }
  // state ist OK — fahre mit Token-Exchange fort
  delete req.session.oauth_state;
  // ... code gegen Tokens tauschen
});

Warum das CSRF verhindert:

  • Eine Angreifer-präparierte Callback-URL kennt das Session-State des Opfers nicht.
  • Wenn der Angreifer einen Link mit einem fremden state schickt, scheitert der Check.
  • Wenn der Angreifer einen Link ohne state schickt, scheitert der Check ebenfalls.

Der state-Parameter ist Pflicht in jedem OAuth-Flow. OAuth 2.0 Spezifikation (RFC 6749) erwähnt ihn als „RECOMMENDED" — OAuth 2.1 (RFC 9700, Best Current Practice 2024) macht ihn faktisch verpflichtend.

PKCE als Ergänzung (und Pflicht in OAuth 2.1)

PKCE (Proof Key for Code Exchange, gesprochen „pixie") ist ein zusätzliches Sicherheits-Verfahren, das ursprünglich für Mobile-Apps entwickelt wurde — heute Pflicht in jedem OAuth-Flow nach OAuth 2.1.

Mechanik:

  1. Client generiert eine zufällige Geheimzahl (code_verifier).
  2. Daraus berechnet er einen Hash (code_challenge).
  3. Beim Authorize-Request schickt er nur die Challenge mit.
  4. Beim Token-Exchange später schickt er den ursprünglichen Verifier mit.
  5. Authorization Server prüft: passt der Hash des Verifiers zur Challenge?

Was das schützt:

PKCE schützt primär gegen Authorization-Code-Interception — wenn ein Angreifer den Code aus dem Callback abfängt, kann er ihn nicht gegen Tokens tauschen, weil er den Verifier nicht hat.

PKCE + state zusammen:

  • state schützt gegen CSRF auf den Callback-Handler.
  • PKCE schützt gegen Code-Interception.

OAuth 2.1 verlangt beides. Wer einen neuen OAuth-Client baut, sollte beides implementieren — die meisten Auth-Libraries (openid-client für Node, Authlib für Python, Spring Security OAuth) machen es automatisch.

JavaScript oauth-pkce.js
function generatePKCE() {
  const verifier = base64url(crypto.randomBytes(32));
  const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
  return { verifier, challenge };
}

app.get('/auth/google', (req, res) => {
  const state = crypto.randomBytes(32).toString('hex');
  const { verifier, challenge } = generatePKCE();
  req.session.oauth_state = state;
  req.session.code_verifier = verifier;

  const url = new URL('https://google.com/oauth2/authorize');
  url.search = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: 'https://app.example/auth/google/callback',
    response_type: 'code',
    scope: 'openid email',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });
  res.redirect(url);
});

SAML — der ältere Cousin

SAML (Security Assertion Markup Language) ist das ältere Auth-Protokoll, vor allem in Enterprise-Single-Sign-On-Umgebungen verbreitet. Es hat ähnliche CSRF-Problematiken — und seinen eigenen Schutz-Parameter: RelayState.

Standard-SAML-Flow:

Plain saml-flow.txt
1. User klickt "SSO Login" in der App (Service Provider, SP)
2. SP erzeugt SAMLRequest, redirected User zum Identity Provider (IdP)
3. IdP authentifiziert User, erzeugt SAMLResponse
4. IdP POSTet (oder redirected) SAMLResponse zurück zu SP/ACS-Endpunkt
5. SP validiert SAMLResponse, loggt User ein

CSRF-Risiko:

Wie bei OAuth kann ein Angreifer:in eine SAML-Response (vom Angreifer-eigenen IdP-Setup oder per fremder Sitzung) an das Opfer schicken — Opfer-Browser POSTet sie an den Service Provider — der SP loggt das Opfer in die Angreifer-Identität ein.

Schutz: RelayState-Parameter.

Analog zum OAuth-state:

  • SP erzeugt zufälligen RelayState-Wert, speichert ihn an Session.
  • Bei Redirect an IdP wird RelayState=<wert> mitgeschickt.
  • IdP gibt ihn unverändert im SAML-Response zurück (entweder als Form-Field oder URL-Param).
  • SP prüft, dass RelayState zur Session passt.

Hinweis: RelayState ist nicht primär als CSRF-Schutz definiert — er wurde für Deep-Link-Erhaltung entworfen (wohin soll der User nach Login gehen?). Aber die Eigenschaft, dass er von SP zu IdP und zurück läuft, macht ihn zur natürlichen CSRF-Schutz-Schicht.

Best Practice (NIST SP 800-63 + OWASP):

  • RelayState mit kryptografisch zufälligem Wert binden.
  • Server-Side-Storage des Werts mit Session-Binding.
  • Validierung im SAML-Response-Handler.
  • Maximal-Größe für RelayState (RFC 7522 sagt 80 Bytes max).

Manche SP-Implementierungen (z. B. ältere Versionen von Shibboleth, Keycloak) hatten in der Vergangenheit unsichere RelayState-Handhabung — CVE-Wellen 2019–2022.

Bekannte Vorfälle

Account-Link-Hijacking in OAuth ist ein häufiger Bug-Bounty-Befund. Beispiele:

GitHub Enterprise (2018) — CSRF in OAuth-Flow ohne state-Parameter. Bug-Bounty-Auszahlung, fünfstellig.

Facebook Login (mehrere Reports 2014–2019) — diverse Bypasses des state-Schutzes durch Redirect-URI-Manipulation und subdomain-Tricks.

Microsoft Azure AD (2020/21) — mehrere CVEs zu unsicherer Session-State-Bindung in OAuth-Flows.

Slack OAuth (2017) — Bug-Report, der das Account-Link-Hijacking-Pattern demonstrierte; danach Slack hat state-Validierung verschärft.

Vielfach bei selbst gebauten Auth-Implementierungen in Mittelstands-Apps — wer OAuth nicht über etablierte Library macht, vergisst oft den state-Check.

Bug-Bounty-Auszahlungen: OAuth-CSRF-Reports liegen typisch im vierstelligen bis fünfstelligen Dollar-Bereich — die Wirkung ist hoch (Account-Übernahme), die Klasse gut definiert, die Reproduktion straightforward.

Weitere OAuth-spezifische CSRF-Vektoren

Über den klassischen state-Check hinaus gibt es weitere CSRF-nahe OAuth-Vektoren:

Redirect-URI-Manipulation:

Wenn die App mehrere Redirect-URIs registriert hat und der Identity-Provider nicht strikt prüft, kann ein Angreifer auf eine andere registrierte URI redirecten lassen. Wenn diese andere URI bei einem ungesicherten Endpoint landet, geht der Authorization-Code an die falsche Stelle.

Schutz: Strict Redirect-URI-Matching im IdP — nur exakte URI-Übereinstimmung, keine Wildcard-Pfade.

Open Redirect in Callback-Handler:

Wenn dein Callback-Handler einen ?next=-Parameter nimmt und nach erfolgreichem Login dort hin redirected, ist das ein Open Redirect. Angreifer kann die Auth-Strecke nutzen, um Phishing-Domains glaubwürdiger zu machen (app.example/callback?...&next=evil.example).

Schutz: next-Parameter strikt auf eigene Pfade validieren — niemals fremde URLs zulassen.

Implicit Flow (deprecated):

OAuth 2.0 hatte einen response_type=token-Modus (Implicit Flow), bei dem der Access-Token direkt im URL-Fragment zurückkam. CSRF-Schutz war schwächer, weil keine Backend-Token-Exchange-Phase. OAuth 2.1 (RFC 9700, BCP 2024) deprecated den Implicit Flow — Authorization Code Flow mit PKCE ist die Pflicht-Variante.

ROPC (Resource Owner Password Credentials, deprecated):

Variante, wo der Client die User-Credentials direkt nimmt und an den IdP schickt. Funktional eine Backdoor um den ganzen Authorization-Flow herum. OAuth 2.1 verbietet ROPC — vollständig.

Mobile-App-Custom-URI-Schemes:

Mobile-Apps registrieren oft Custom-URI-Schemes (myapp://) als Redirect-URI. Diese sind nicht eindeutig — mehrere Apps können das gleiche Scheme registrieren. Angreifer-App kann den Code abfangen. PKCE schließt das, weil ohne code_verifier der Code nicht eingelöst werden kann.

Schutz: Nur Universal Links (iOS) / App Links (Android) statt Custom-URI-Schemes. PKCE.

Praktische Empfehlungen

Für eine neue OAuth-Integration im Jahr 2026:

Pflicht-Checkliste:

  • OAuth 2.1 / OIDC als Referenz-Standard.
  • Authorization-Code-Flow (nicht Implicit, nicht ROPC).
  • PKCE für alle Clients (auch Confidential Clients).
  • state-Parameter mit kryptografisch zufälligem Wert.
  • Strict Redirect-URI-Whitelist beim Identity-Provider.
  • nonce-Parameter für OIDC ID-Token-Replay-Schutz.
  • Server-Side-Session-Binding von state und code_verifier.
  • next-Parameter validieren — kein Open Redirect.

Library-Empfehlungen:

  • Node.js: openid-client (Panva, sehr ausgereift).
  • Python: Authlib oder Django-OIDC-Provider.
  • Java/Spring: Spring Security OAuth 2 Client.
  • PHP: league/oauth2-client (Symfony-Ökosystem).
  • Ruby: OmniAuth + omniauth-oauth2.
  • Go: golang.org/x/oauth2.

Diese Libraries handhaben state, PKCE, Token-Refresh standardmäßig korrekt. Wer eigene OAuth-Implementation baut, hat ein signifikant höheres Bug-Risiko.

Was du nicht selbst tun solltest:

  • OAuth-Spezifikation aus dem Gedächtnis implementieren.
  • state-Check auslassen, weil „SameSite das schon macht".
  • Cookie-Auth ohne state für OAuth (CSRF-Tor).
  • Redirect-URIs mit Wildcards oder Pfad-Matching.

Besonderheiten

OAuth 2.1 fasst die Best Practices zusammen

RFC 9700 (Januar 2024) ist die OAuth 2.1 Best Current Practice. Sie konsolidiert OAuth 2.0 mit den Sicherheits-Erkenntnissen aus 15 Jahren Praxis — PKCE ist Pflicht, Implicit Flow ist out, Redirect-URIs müssen exakt matchen. Wer einen modernen OAuth-Client baut, sollte RFC 9700 als Leitfaden nehmen.

state-Parameter wird oft falsch gemacht

Häufige Fehler: state aus User-Eingabe konstruiert (statt random); state ohne Session-Binding (in localStorage gespeichert, gegen XSS unkontrolliert lesbar); state nicht gegen Sessions geprüft, nur „ist es vorhanden". Alle drei brechen den CSRF-Schutz. Library nutzen, nicht selbst bauen.

PKCE schützt mehr als nur CSRF

PKCE wurde primär für Mobile-Apps mit Custom-URI-Schemes entworfen, um Code-Interception zu verhindern. Es schützt auch in Browser-OAuth gegen einige Edge-Cases — z. B. wenn ein Angreifer durch Sub-Domain-Kompromittierung an den Code kommt. OAuth 2.1 verlangt PKCE für alle Clients, nicht nur Public Clients.

Identity-Provider-Hijacking als verwandtes Risiko

Wenn dein App-Konto an einen Identity-Provider-Account gebunden ist und der IdP-Account kompromittiert wird, ist auch der App-Account weg. Konsequenz: kritische Konten sollten nicht nur OAuth-Login haben, sondern auch ein eigenes Passwort als Backup-Pfad.

SAML-Replay-Angriffe

Eine verwandte Klasse: SAML-Responses haben eine Lebensdauer und sollten nur einmal verwendet werden. Wenn der SP nicht den NotOnOrAfter-Timestamp und die ID-Eindeutigkeit prüft, kann eine abgefangene SAML-Response mehrfach verwendet werden. RelayState hilft nur teilweise; die SAML-Response selbst muss replay-resistent geprüft werden.

OAuth-Misuse-Studies

Daniel Fett und Ralf Küsters haben in mehreren akademischen Veröffentlichungen (2014–2020) systematisch OAuth-Schwachstellen in realen Apps analysiert. Ihr Tool OAuth-Tools testet Implementations gegen die wichtigsten Klassen — sehr wertvoll für Audit-Workflows.

Mobile-OAuth: Universal Links statt Custom Schemes

Custom-URI-Schemes wie myapp://callback sind anfällig — andere Apps können das Scheme entführen. iOS Universal Links (associated domains) und Android App Links (Digital Asset Links) sind verifizierbar an deine Domain gebunden. Moderne OAuth-Clients für Mobile sollten ausschließlich diese nutzen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu CSRF & SameSite

Zur Übersicht