Cybersicherheit

Sichere Cookies in PHP — Dem Browser weniger Risiko überlassen

Sichere Cookies in PHP — Dem Browser weniger Risiko überlassen

Ein Cookie wirkt klein, hat aber eine ungewöhnliche Eigenschaft: Sobald ein Browser es akzeptiert, kann er es automatisch an spätere Requests anhängen. Das ist für eine Login-Session praktisch. Gleichzeitig muss eine Anwendung genau festlegen, wohin das Cookie gesendet werden darf und welcher Code darauf zugreifen kann.

Für eine kleine PHP-Anwendung lautet die nützliche Frage nicht „Welches Sicherheits-Flag soll ich kopieren?“, sondern: „Welches Verhalten brauche ich, und wie klein kann der dafür nötige Geltungsbereich sein?“ Die Cookie-Spezifikation, die PHP-Dokumentation zu setcookie() und die Sicherheitsempfehlungen von OWASP sind dafür ein guter Ausgangspunkt.

Was ein Session-Cookie tatsächlich tut

HTTP merkt sich einen Benutzer zwischen Requests nicht von selbst. Ein Server kann den Response-Header Set-Cookie senden; der Browser kann den Wert später in einem Cookie-Request-Header zurücksenden. In einem üblichen Session-Design ist dieser Wert ein opaker Identifier. Der Server speichert Benutzer, Ablaufzeit und Berechtigungsstatus getrennt und sucht den Identifier bei jedem Request nach.

Diese Unterscheidung ist wichtig. Ein Cookie ist nicht automatisch ein Passwort, ein verschlüsselter Container oder ein Beweis für einen vertrauenswürdigen Request. Meist ist es ein Träger von Zustandsinformationen: Wer einen noch gültigen Session-Identifier vorlegt, kann bis zur Ablehnung oder Beendigung durch den Server als Eigentümer der Session behandelt werden. Cookie-Attribute verringern einige Möglichkeiten, wie der Identifier verloren geht oder gesendet wird. Einen gestohlenen, noch gültigen Identifier machen sie nicht wirkungslos.

Mit Secure und HttpOnly beginnen

Secure weist den Browser an, das Cookie nur über eine sichere Verbindung zu senden, normalerweise HTTPS. Dadurch wird verhindert, dass ein Session-Identifier über einen unverschlüsselten HTTP-Request übertragen wird. Das Attribut verschlüsselt den Wert nicht an jedem Ort, an dem er existieren kann, und behebt keine Anwendung, die einen vom Angreifer kontrollierten Session-Identifier akzeptiert.

HttpOnly verhindert, dass gewöhnliche Browser-Skripte das Cookie über APIs wie document.cookie lesen. Das ist nützlich, wenn das Cookie einen Session-Identifier enthält. Es ist jedoch kein Schutz gegen XSS. Eingeschleuster Code kann weiterhin Requests aus dem Browser des Opfers auslösen, und der Browser kann das HttpOnly-Cookie an diese Requests anhängen. Das Flag schützt das direkte Auslesen des Cookies, nicht jede Handlung von Code, der in der Seite läuft.

Beide Attribute lösen unterschiedliche Probleme und können zusammen verwendet werden:

Set-Cookie: __Host-Session=opaque-value; Secure; HttpOnly; SameSite=Lax; Path=/

Der obige Wert zeigt nur die Form eines Headers. Er ist keine vollständige Session-Implementierung. Der Server muss weiterhin schwer erratbare Identifier erzeugen, sie validieren, Berechtigungen erzwingen, sie nach der Anmeldung wechseln und sie bei Logout oder Ablauf ungültig machen.

SameSite bewusst auswählen

Das Attribut SameSite steuert, ob der Browser ein Cookie an siteübergreifende Requests anhängt. „Same-site“ und „same-origin“ sind verwandte, aber nicht austauschbare Begriffe. Das aktuelle RFC-6265bis-Dokument ist noch ein Internet-Draft. Seine Details dürfen daher nicht als endgültige RFC-Norm behandelt werden. Die Beschreibung entspricht der aktuellen browserbezogenen Dokumentation zu den drei bekannten Werten:

  • Strict ist die engste Wahl. Das Cookie wird bei siteübergreifenden Requests nicht gesendet, was den Einstieg von einer anderen Website erschweren kann.
  • Lax erlaubt Same-site-Requests und bestimmte Top-Level-Navigationen von einer anderen Website, schließt aber viele siteübergreifende Subresource- und unsichere Methoden aus.
  • None erlaubt siteübergreifendes Senden und muss mit Secure kombiniert werden. Es ist für Fälle gedacht, die dieses Verhalten wirklich benötigen, nicht als Standard zum Verbergen von Integrationsfehlern.

Für eine normale First-Party-Login-Session sind Strict oder Lax meist ein engerer Ausgangspunkt als None. Die richtige Wahl hängt vom Navigations- und Integrationsmodell der Anwendung ab. OWASP beschreibt SameSite als Defense in Depth gegen CSRF, nicht als Ersatz für ein serverseitig geprüftes CSRF-Token. Ein Endpoint, der den Zustand verändert, muss weiterhin einen geeigneten CSRF-Schutz und die Autorisierung prüfen.

Domain und Path klein halten

Wenn Domain fehlt, ist das Cookie host-only: Es ist für den Host bestimmt, der es gesetzt hat. Domain=example.com erweitert die Übertragung auf Subdomains wie admin.example.com und shop.example.com. Das kann für eine gemeinsame Anwendung notwendig sein, macht aber auch eine schwächere Subdomain für die Sicherheit des Cookies relevant.

Der kleinste sinnvolle Geltungsbereich ist normalerweise der sicherere Standard. Füge Domain nicht nur deshalb hinzu, weil Beispiele im Internet es tun. Wenn eine Session nur zu app.example.com gehört, vergrößert der Empfang durch alle Subdomains den möglichen Schaden ohne zusätzlichen Nutzen.

Path steuert, welche URL-Pfade das Cookie erhalten. Es kann unnötige Übertragung reduzieren, ist aber keine Zugriffskontrollgrenze. Code auf demselben Host kann weiterhin mit anderen Pfaden interagieren, und das Cookie-Modell von Browsern hat historische Sonderfälle. Behandle Path als Routing- und Bandbreitenbereich, nicht als Ersatz für Autorisierung.

Zum Ablauf nicht auf das Schließen des Browsers vertrauen

Ein Cookie ohne Expires oder Max-Age wird häufig Session-Cookie genannt. Das kann irreführend sein: Die Sitzungswiederherstellung eines Browsers kann es beim Schließen und erneuten Öffnen erhalten. Wenn eine Session nach einer bestimmten Zeit ablaufen muss, muss der Server diese Lebensdauer ebenfalls erzwingen.

Bei persistenten Cookies beschreibt Max-Age eine Dauer und hat Vorrang vor Expires, wenn beide vorhanden sind. Zum Löschen muss der Server ein neues Cookie mit demselben Namen und demselben Geltungsbereich senden und eine vergangene Ablaufzeit oder ein nicht positives Max-Age setzen. Das Löschen der Browserkopie macht eine bereits gestohlene Kopie nicht ungültig; der Server muss den zugehörigen Session-Datensatz widerrufen oder beenden.

Das Options-Array von PHP verwenden

PHP unterstützt die Options-Array-Form von setcookie() seit PHP 7.3. Das offizielle Handbuch dokumentiert die Optionen expires, path, domain, secure, httponly und samesite. Der Aufruf muss vor jeder Ausgabe erfolgen, weil er einen HTTP-Response-Header erzeugt.

<?php

$sessionId = generate_session_id(); // Eine CSPRNG-basierte Implementierung verwenden.

setcookie('__Host-Session', $sessionId, [
    'expires' => 0,       // Session-Cookie; Ablauf auch serverseitig erzwingen.
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

Der Name generate_session_id() ist absichtlich ein Platzhalter und keine eingebaute PHP-Funktion. Die Anwendung muss einen kryptografisch sicheren Generator oder den etablierten Session-Mechanismus ihres Frameworks verwenden. Benutzername, Rolle, E-Mail-Adresse oder andere sensible Daten sollten nicht direkt im Cookie stehen, nur weil der Browser sie speichern kann.

Das Präfix __Host- fügt in unterstützenden User-Agents browserseitig erzwungene Anforderungen hinzu: Das Cookie muss Secure verwenden, Path=/ besitzen und darf kein Domain-Attribut haben. Das Präfix ist eine sinnvolle Defense-in-Depth-Maßnahme, aber kein Ersatz für Tests der Browserunterstützung oder serverseitige Session-Kontrollen.

Was dieses Beispiel nicht löst

Eine sicher aussehende Set-Cookie-Zeile kann trotzdem in einer unsicheren Anwendung stehen. Der Server muss den empfangenen Identifier mit einem serverseitigen Session-Datensatz vergleichen, abgelaufene oder widerrufene Sessions ablehnen und den Identifier nach Login oder Privilegänderungen erneuern, um das Risiko von Session Fixation zu verringern. Session-Identifier sollten außerdem nicht in URLs akzeptiert werden, weil sie in Verlauf, Logs, Lesezeichen oder Referrer-Daten auftauchen können.

Eine weitere Grenze ist wichtig: HttpOnly verhindert kein XSS, SameSite ersetzt keine CSRF-Prüfung, und Secure macht keinen HTTP-Endpoint sicher. Das sind mehrere Schutzschichten. Die Anwendung braucht weiterhin Output-Encoding, Eingabevalidierung, Autorisierungsprüfungen, TLS während der gesamten Session und einen Logout-Weg, der den serverseitigen Zustand ungültig macht.

Kurze Review-Checkliste

  • Ist der Session-Wert opak und mit einem geeigneten kryptografisch sicheren Verfahren erzeugt?
  • Verwendet das Session-Cookie Secure und HttpOnly, wenn JavaScript-Zugriff nicht erforderlich ist?
  • Wurde SameSite ausdrücklich anhand des tatsächlich benötigten siteübergreifenden Verhaltens gewählt?
  • Wurde Domain weggelassen, sofern das Teilen zwischen Subdomains keine bewusste Anforderung ist?
  • Wird Path als Übertragungsbereich und nicht als Autorisierung behandelt?
  • Werden Ablauf, Erneuerung, Logout und Widerruf serverseitig erzwungen?
  • Sind CSRF- und XSS-Schutz getrennt implementiert?
  • Verwendet die Löschantwort denselben Namen sowie dieselben Domain- und Path-Werte wie das zu löschende Cookie?

Fazit

Cookie-Sicherheit bedeutet vor allem, automatisches Verhalten zu begrenzen. Secure begrenzt den Transport, HttpOnly die Sichtbarkeit für Scripts, SameSite die siteübergreifende Übertragung, und ein enger Host- und Pfadbereich die vorgesehenen Empfänger. Kein Attribut kann dem Server mitteilen, ob ein Request autorisiert ist oder eine gestohlene Session noch gültig sein sollte.

Für eine kleine PHP-Anwendung ist das ehrliche Ziel daher kein magisches Cookie, das Web-Sicherheit vollständig löst. Es ist ein bewusst begrenzter Träger einer opaken serverseitigen Session, umgeben von unabhängigen Prüfungen. Das klingt vielleicht weniger eindrucksvoll als ein weiteres Flag, aber jede Kontrolle erhält eine Aufgabe, die tatsächlich überprüft werden kann.

References