Während SameSite-Cookies eine Browser-seitige Schutz-Schicht sind, ist das CSRF-Token-Pattern die klassische Anwendungs-seitige Verteidigung — bewährt seit den frühen 2000ern, eingebaut in fast alle modernen Web-Frameworks. Wer eine traditionelle Form-basierte App baut, hat damit eine zweite, unabhängige Schutz-Schicht. Dieser Artikel zeigt die drei verbreiteten Patterns, ihre Trade-offs und die typischen Implementierungs-Fehler.

Das Grundprinzip

Ein CSRF-Token ist ein unvorhersehbarer Wert, den der Server erzeugt und der bei sensiblen Anfragen mitgeschickt werden muss. Der Server prüft, dass das Token zur aktuellen Session gehört und nicht abgelaufen ist.

Warum das CSRF verhindert: Eine Angreifer-Seite kann zwar Cookies cross-site mitsenden (oder konnte es vor SameSite), aber sie kennt das Token nicht — die Same-Origin-Policy verhindert, dass JavaScript auf evil.example die Page von bank.example liest und das Token extrahiert.

Klassisches Pattern (Synchronizer-Token):

HTML csrf-token-form.html
<!-- Server-rendered Form mit Token -->
<form method="POST" action="/transfer">
  <input type="hidden" name="csrf_token" value="r4nd0m-512-bit-value">
  <input name="to" value="...">
  <input name="amount" value="...">
  <button>Senden</button>
</form>

Server-Logik:

Python csrf-server-check.py
@app.post('/transfer')
def transfer():
  # Token aus Form-Body
  submitted = request.form.get('csrf_token')
  # Token aus Server-State (Session)
  expected = session.get('csrf_token')

  if not submitted or submitted != expected:
    abort(403, 'CSRF-Token ungültig')

  # ... eigentliche Logik

Der/die Angreifer:in auf evil.example kann die Form per JavaScript abschicken — aber das Token-Feld bleibt leer. Server lehnt ab.

Das Synchronizer-Token-Pattern

Das klassische und sicherste Pattern. Drei Anforderungen:

1. Token wird vom Server erzeugt — kryptografisch zufällig, mindestens 128 Bit Entropie (besser 256).

2. Token wird im Server-State gespeichert — typischerweise in der Session. Server merkt sich pro User-Session ein Token.

3. Token wird bei jeder schreibenden Anfrage geprüft — Mismatch oder Fehlen → Anfrage abgelehnt.

Token-Erzeugung in Python:

Python csrf-token-generation.py
import secrets

def generate_csrf_token():
  return secrets.token_urlsafe(32)  # 32 Bytes = 256 Bit

@app.before_request
def ensure_csrf_token():
  if 'csrf_token' not in session:
    session['csrf_token'] = generate_csrf_token()

Pro Form ausgeben:

HTML csrf-token-template.html
<form method="POST">
  <input type="hidden" name="csrf_token" value="{{ session.csrf_token }}">
  <!-- ... -->
</form>

Token-Rotation:

Optional rotiert man das Token nach jeder erfolgreichen Form-Submission oder bei Login. Das verhindert Token-Reuse-Angriffe und reduziert die Lebensdauer eines geleakten Tokens.

Vorteile des Synchronizer-Pattern:

  • Sehr stark — Token muss exakt mit Server-State übereinstimmen.
  • Resistent gegen Cookie-Manipulation — Token ist nicht im Cookie, sondern im Body.
  • Standard in den meisten Frameworks — Django, Rails, Spring, Laravel.

Nachteile:

  • Stateful — Server muss pro Session ein Token speichern. Schwer in stateless-Architekturen (z. B. Multi-Region-Backend ohne sticky Sessions).
  • Bei vielen parallel offenen Tabs kann Token-Mismatch auftreten — wenn das Token rotiert wird, alte Tab-Token werden ungültig.

Eine stateless-Variante: Das Token wird sowohl in einem Cookie als auch im Request-Body mitgeschickt. Server prüft, dass beide übereinstimmen.

Pattern:

JavaScript double-submit-flow.js
// 1. Server schickt Cookie beim ersten Page-Load
Set-Cookie: csrf_token=r4nd0m-value; SameSite=Lax; Secure

// 2. Client-JS liest das Cookie und setzt es in Form/Header
const token = document.cookie.match(/csrf_token=([^;]+)/)?.[1];
fetch('/api/transfer', {
  method: 'POST',
  headers: { 'X-CSRF-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({ to: '...', amount: 100 }),
});

// 3. Server prüft, dass Cookie-Wert und Header-Wert übereinstimmen

Server-Check (Express-Pseudo):

JavaScript double-submit-check.js
app.post('/api/transfer', (req, res) => {
  const cookieToken = req.cookies.csrf_token;
  const headerToken = req.headers['x-csrf-token'];

  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return res.status(403).send('CSRF rejection');
  }

  // ... eigentliche Logik
});

Warum das CSRF verhindert:

  • Eine Angreifer-Seite kann das Cookie nicht lesen (Same-Origin-Policy).
  • Sie kann das Cookie zwar bei einer Cross-Site-Anfrage mitsenden lassen (wenn SameSite=None), aber sie kann nicht das Header-Feld setzen mit dem passenden Wert.
  • Token-Cookie und Header-Token sind nur dann gleich, wenn die Anfrage von eigenem JavaScript erzeugt wurde — und das läuft auf der eigenen Origin.

Vorteile:

  • Stateless — keine Server-Session-Speicherung nötig.
  • Skaliert horizontal — beliebig viele Backend-Instanzen, keine Sticky-Sessions.
  • Funktioniert mit JSON-APIs.

Nachteile und Stolperfallen:

  • Cookie muss vom Server gesetzt werden — und der Browser sendet es zurück. Das ist nicht trivial in komplexen Setups.
  • Schwächer als Synchronizer-Pattern, wenn der Angreifer das Token-Cookie setzen kann (z. B. via Cookie-Tossing-Angriffe von Sub-Domains, XSS).
  • HttpOnly ist hier nicht möglich — JS muss das Cookie lesen, sonst kann es nicht im Header gesendet werden.

Signiertes Double-Submit als Verbesserung:

JavaScript signed-double-submit.js
// Token = HMAC(server_secret, session_id + random_value)
// Cookie enthält das Token mit der Signatur
// Server prüft Signatur + Übereinstimmung

function generateToken(sessionId, secret) {
  const random = crypto.randomBytes(16).toString('hex');
  const hmac = crypto.createHmac('sha256', secret)
    .update(`${sessionId}.${random}`)
    .digest('hex');
  return `${random}.${hmac}`;
}

Damit kann der Server prüfen, dass das Token kryptografisch zu seiner Session gehört — auch wenn ein Angreifer das Cookie setzen könnte, kann er keine gültige Signatur erzeugen.

Encrypted Token Pattern

Eine Variante des Double-Submit-Patterns, die das Token verschlüsselt im Cookie speichert. Server kann das Token entschlüsseln und mit Session-Daten vergleichen.

Python encrypted-token.py
from cryptography.fernet import Fernet

SECRET_KEY = Fernet(b'...')  # Server-Geheimnis

def generate_encrypted_token(session_id):
  plain = f"{session_id}|{int(time.time())}"
  return SECRET_KEY.encrypt(plain.encode()).decode()

def verify_encrypted_token(token, session_id):
  try:
    plain = SECRET_KEY.decrypt(token.encode()).decode()
    stored_session, timestamp = plain.split('|')
    if stored_session != session_id:
      return False
    if int(time.time()) - int(timestamp) > 3600:
      return False  # Token zu alt
    return True
  except Exception:
    return False

Vorteile gegenüber unsigniertem Double-Submit:

  • Session-Binding kryptografisch garantiert.
  • Token-Verfallsdatum integriert.
  • Keine Server-Session-Tabelle für CSRF-Tokens nötig.

Dieses Pattern ist die stateless-Alternative, die heute in vielen modernen Frameworks (z. B. Rails 5+) verbreitet ist.

Framework-Defaults

Die meisten modernen Web-Frameworks haben CSRF-Schutz eingebaut:

FrameworkDefault-PatternWie aktiviert
DjangoSynchronizer-Token mit Session-StateMiddleware aktiv per Default; {% csrf_token %} in Template
RailsSynchronizer-TokenActive per Default; <%= csrf_meta_tags %> und automatische Form-Helpers
Spring SecuritySynchronizer-Token + Double-Submit-OptionCsrfFilter aktiv per Default seit Spring Security 4.0
LaravelSynchronizer-Token@csrf in Blade-Templates; Middleware aktiv
ASP.NET CoreSigned Double-Submit (AntiForgery)Eingebaut; [ValidateAntiForgeryToken]-Attribut
Express.jsKeine Default — Middleware nötigcsurf-Paket (deprecated; Empfehlung: csrf-csrf-Paket)
Next.js / RemixKeine Default — eigene ImplementationManuell mit csrf-csrf oder ähnlich
Hapi.jscrumb-PluginManuell aktivieren
FlaskKeine Default — Extension nötigFlask-WTF mit CSRFProtect
FastAPIKeine DefaultManuell mit Middleware

Praktische Konsequenz:

  • In Django, Rails, Spring, Laravel: CSRF-Schutz ist Default — wer es nicht explizit deaktiviert, ist geschützt.
  • In Express, Flask, FastAPI: Du musst aktiv eine Middleware installieren und konfigurieren. Häufiger Vergessen-Fehler.

Wenn dein Framework keinen Default hat: csrf-csrf (Express, ersetzt das deprecated csurf), Flask-WTF, oder eigene Implementierung mit Signed-Double-Submit-Pattern.

Token-Übertragung — Body, Header, beides?

Wo soll das Token mitgeschickt werden?

Body-Parameter (Form-Field):

HTML csrf-body.html
<form method="POST">
  <input type="hidden" name="csrf_token" value="...">
  <!-- andere Felder -->
</form>
  • Klassisch für Form-basierte Apps.
  • Funktioniert mit standardem application/x-www-form-urlencoded und multipart/form-data.

Custom Header:

JavaScript csrf-header.js
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': token,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data),
});
  • Klassisch für SPA-/JSON-API-Workflows.
  • Custom-Header ist als zusätzliches Anti-CSRF-Signal — Browser löst CORS-Preflight aus, was Cross-Site-Anfragen ohne CORS-Allowance verhindert.

URL-Query-Parameter — VERMEIDEN:

Token in URL landet in:

  • Server-Logs.
  • Browser-History.
  • Referrer-Header bei weiteren Klicks.
  • Geräte-Synchronisation.

→ Tokens nicht in URLs.

Mehrere Übertragungs-Wege erlauben:

Manche Frameworks akzeptieren Tokens entweder im Body oder im Header. Das macht den Code flexibel — sowohl traditionelle Forms als auch SPA-Aufrufe funktionieren.

Häufige Implementierungs-Fehler

Fehler 1 — Token in URL-Query-String. Wie oben: landet in Logs und History.

Fehler 2 — Token in GET-Antwort nicht regenerieren. Wenn das Token bei jedem GET die gleiche Sequenz hat, ist es eventuell erratbar oder veraltet.

Fehler 3 — Token in HTTPS gesetzt, aber via HTTP zurückgesendet. Secure-Cookie-Flag nicht vergessen. HTTPS überall.

Fehler 4 — Token-Check übersehen für bestimmte HTTP-Methoden. Häufiger Bug: nur POST-Endpunkte werden geprüft, PUT/DELETE/PATCH bleiben offen. Alle zustandsändernden Methoden müssen geprüft werden.

Fehler 5 — Token-Vergleich mit == statt constant-time-compare. Theoretisch ist String-Vergleich mit Frühabbruch ein Timing-Side-Channel. Bei modernen Tokens (256 Bit zufällig) praktisch irrelevant — aber secrets.compare_digest (Python) oder crypto.timingSafeEqual (Node) sind sauberer.

Fehler 6 — Token bei Login nicht rotieren. Wenn ein Angreifer das Token einer noch-nicht-eingeloggten Session kennt (z. B. von einer öffentlich erreichbaren Seite), bleibt es nach dem Login gültig. Bei Login Token rotieren.

Fehler 7 — Token für AJAX-Calls nicht gesendet. Klassischer Bug: Form-Endpunkte sind geschützt, aber AJAX-Endpunkte vergessen. Pro Endpoint prüfen.

Fehler 8 — Token-Validierung-Bypass durch leeren Token-Header. Wenn die Middleware Header == StoredToken prüft, und beide leer sind, passiert es: '' == ''. Manche Pakete haben das gefixt; explizit prüfen, dass Token nicht leer ist.

CSRF-Token-Lebensdauer

Wie lange soll ein Token gültig sein?

Pro Session-Token (Default in vielen Frameworks):

  • Eine Token-Wert pro Session.
  • Lebt so lange wie die Session.
  • Einfach, funktioniert für die meisten Apps.

Pro Form / pro Request:

  • Jede Form bekommt eigenes Token.
  • Token wird nach Verwendung invalidiert.
  • Schutz gegen Token-Replay.
  • Höherer Server-State-Overhead.
  • Kann bei vielen parallelen Tabs Probleme machen.

Zeit-begrenzt:

  • Token hat Verfallsdatum (z. B. 1 Stunde).
  • Erzwingt regelmäßige Erneuerung.
  • Reduziert Schaden bei Token-Leak.

In der Praxis ist pro-Session die häufigste Wahl. Wer sehr sensitive Aktionen hat (Banken, Krypto-Wallets), nutzt pro-Form mit kurzer Lebensdauer.

Interessantes

Synchronizer-Token vs. Double-Submit — Trade-off-Übersicht

Synchronizer ist sicherer (Server-State, kryptografische Bindung an Session). Double-Submit ist skalierbarer (stateless, gut für horizontale Architekturen). Wer beides braucht: Signed Double-Submit mit HMAC kombiniert die Vorteile.

csurf in Express ist deprecated

Das beliebte csurf-Paket für Express wurde Ende 2022 als deprecated markiert. Aktuelle Empfehlung: csrf-csrf (Signed Double-Submit) oder eigene Implementation. Wer noch csurf in Produktion hat: Migration einplanen.

Spring Security 6 hat CSRF-Defaults strikter gemacht

Spring Security 6 (2022) hat das CSRF-Verhalten gestrafft — manche legacy-Konfigurationen brechen ohne explizites Opt-In. Wer migriert: HttpSecurity.csrf()-Konfiguration prüfen.

Django CSRF + SPA = Aufmerksamkeit

Wer ein Django-Backend mit React/Vue-Frontend nutzt, muss das CSRF-Token aus dem Cookie lesen und in den AJAX-Header setzen. Django setzt das Cookie standardmäßig HttpOnly: false, damit JS es lesen kann. Wer das auf HttpOnly: true setzt, bricht den Workflow.

JSON-APIs ohne Cookie-Auth — kein CSRF-Token nötig

Wenn deine API Bearer-Tokens (JWT) per Authorization-Header nutzt und keine Cookies für Auth verwendet, hast du strukturell kein CSRF-Risiko. Browser sendet den Authorization-Header nicht automatisch cross-site. CSRF-Tokens wären redundant.

GraphQL-CSRF — separates Thema

GraphQL-Endpunkte sind typisch POST mit JSON-Body und Custom-Content-Type (application/json) — was klassisches CSRF erschwert. Wer Apollo Server mit Cookies-Auth nutzt, muss trotzdem CSRF-Token zusätzlich. Apollo hat in v3 standardmäßig CSRF prevention aktiviert.

CSRF-Token bei File-Uploads

multipart/form-data-Uploads tragen das Token typisch als versteckten Form-Field mit. Achten: das Token muss vor der Datei im Form-Body kommen, sonst kann der Parser-Workflow es übersehen. Frameworks handhaben das meist korrekt.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu CSRF & SameSite

Zur Übersicht