Sichere PHP-Sessions für kleine Anwendungen — Die ID von der Anmeldung bis zur Abmeldung schützen
Ein Anmeldeformular kann das richtige Passwort verifizieren, während das Konto dennoch auf einer schwachen Session beruht. Der schwierige Teil beginnt erst nach der Authentifizierung: Wie erkennt die Anwendung die nächste Anfrage, wie lange darf diese Zuordnung gültig bleiben und was geschieht bei der Abmeldung?
Bei einer kleinen nativen PHP-Anwendung ist die Antwort häufig eine serverseitige Session, deren Kennung in einem Cookie übertragen wird. Das ist ein vernünftiger Entwurf, doch diese Kennung ist vorübergehend fast so nützlich wie die Anmeldedaten, durch die sie entstanden ist. OWASP beschreibt eine authentifizierte Session-ID deshalb als vorübergehend gleichwertig mit der zugrunde liegenden Authentifizierungsmethode. Kann jemand diese Kennung stehlen, vorhersagen oder fixieren, schützt die Passwortprüfung die folgenden Anfragen möglicherweise nicht mehr.
Dies ist kein Rezept für „vollkommen sichere Sessions“. Eine einzelne Konfiguration kann das nicht leisten. Es ist ein praktischer Ansatz, um den gesamten Lebenszyklus zu prüfen: eine ID erzeugen, sie in einem Cookie übertragen, sie bei einer Änderung der Berechtigungen erneuern, sie serverseitig ablaufen lassen und bei der Abmeldung entfernen.
Mit dem Session-Lebenszyklus beginnen, nicht mit einer Liste von Flags
Ein hilfreiches Denkmodell ist eine Garderobenmarke. Der Browser hält die Marke, die Anwendung verwahrt den Mantel und die zugehörigen Informationen. Die Marke selbst sollte nichts Wertvolles offenlegen, doch wer eine gültige Marke besitzt, kann als deren Eigentümer behandelt werden. Deshalb muss auch eine undurchsichtige Session-ID sorgfältig geschützt werden.
Der Lebenszyklus umfasst mindestens fünf Übergänge:
- Eine anonyme Anfrage erhält eine Session oder setzt sie fort.
- Eine erfolgreiche Anmeldung ändert die Session von anonym zu authentifiziert.
- Normale Anfragen verwenden die authentifizierte Session weiter.
- Inaktivität oder eine absolute Frist macht die Session ungültig.
- Die Abmeldung entfernt Anwendungszustand, Browser-Cookie und serverseitigen Datensatz.
Ein sicheres Cookie im ersten Schritt behebt keine fehlende ID-Erneuerung im zweiten. Ein guter Inaktivitäts-Timeout behebt keinen Logout-Handler, der das Browser-Cookie zurücklässt. Die Prüfung der Übergänge macht Lücken sichtbar, die eine Checkliste isolierter Einstellungen verbergen kann.
Die Session vor ihrem Start absichern
PHP erlaubt die Session-Konfiguration in php.ini, in der Pool-Konfiguration oder im Anwendungscode. Verwendet eine Anwendung session_set_cookie_params(), muss die Funktion laut PHP-Handbuch vor session_start() ausgeführt werden; ihre Wirkung gilt nur für das aktuelle Skript. Ein zentraler Bootstrap, den jede authentifizierte Route einbindet, ist sicherer als geringfügig unterschiedliche Einstellungen in mehreren Controllern.
<?php
declare(strict_types=1);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
Dieses Beispiel setzt voraus, dass die Website ausschließlich über HTTPS bereitgestellt wird. Auf einem Entwicklungs-Origin mit einfachem HTTP verhält sich ein Secure-Cookie nicht wie ein Produktions-Cookie. Tests sollten deshalb lokales HTTPS verwenden, statt die Produktionseinstellungen abzuschwächen.
Was die einzelnen Einstellungen tatsächlich bewirken
session.use_strict_mode weist PHP an, keine Session-ID zu übernehmen, die der Session-Handler nicht initialisiert hat. PHP bezeichnet den strikten Modus als wichtige Härtungseinstellung, beschreibt ihn jedoch als Gegenmaßnahme und nicht als vollständige Lösung gegen Session Fixation. session.use_only_cookies verhindert, dass PHP die Kennung über alternative Wege wie Query- oder Formularparameter akzeptiert. Dadurch gelangen IDs außerdem nicht in URLs, den Browser-Verlauf, kopierte Links und übliche Zugriffsprotokolle.
Die Cookie-Attribute reduzieren unterschiedliche Risiken:
Securebeschränkt die Übertragung auf sichere Verbindungen.HttpOnlyverhindert, dass Browser-Skripte das Cookie über APIs wiedocument.cookielesen.SameSite=Laxschränkt einen Teil der websiteübergreifenden Cookie-Übertragung ein, während übliche Navigationen auf oberster Ebene funktionieren.
Diese Kontrollen sind enger begrenzt, als ihre Namen vermuten lassen. HttpOnly behebt kein Cross-Site Scripting: Eingeschleuster Code kann weiterhin Aktionen von der Seite des Opfers ausführen, auch wenn er die ID nicht lesen kann. SameSite ist Defense in Depth und kein Ersatz für CSRF-Token. Secure erfordert HTTPS für die gesamte Session, nicht nur für die Login-Antwort.
Im Beispiel fehlt Domain absichtlich. Nach den Cookie-Regeln aus RFC 6265 bleibt das Cookie dadurch auf den Host beschränkt, der es gesetzt hat. Eine übergeordnete Domain macht es für Subdomains verfügbar und vergrößert die Zahl der Anwendungen, die es beeinflussen können. Ein breiterer Geltungsbereich sollte eine ausdrückliche Anforderung und keine bequeme Voreinstellung sein.
Eine Lebensdauer von null erzeugt ein nicht persistentes Cookie, bedeutet aber nicht, dass „der Benutzer beim Schließen des letzten Tabs sicher abgemeldet ist“. MDN weist darauf hin, dass Browser das Ende einer Session definieren und eine Sitzungswiederherstellung solche Cookies erhalten kann. Ein serverseitiger Ablauf bleibt notwendig.
Die ID erneuern, wenn sich das Vertrauen ändert
Die Session-ID eines anonymen Besuchers sollte nicht unverändert zur authentifizierten ID werden. Nach der Verifizierung der Anmeldedaten muss sie erneuert werden, bevor nachfolgende Anfragen als authentifiziert gelten:
if ($credentialsAreValid) {
if (!session_regenerate_id(false)) {
throw new RuntimeException('Could not rotate the session ID');
}
$_SESSION['user_id'] = $userId;
$_SESSION['authenticated_at'] = time();
$_SESSION['last_activity_at'] = time();
}
Die Erneuerung ist nach der Anmeldung und nach anderen Berechtigungsänderungen wichtig, etwa beim Wechsel in einen Administratormodus. Sie unterbricht den einfachen Ablauf einer Session Fixation: Ein Angreifer liefert eine bekannte anonyme ID, wartet auf die Authentifizierung durch das Opfer und verwendet die ID anschließend erneut.
Der boolesche Parameter verdient mehr Aufmerksamkeit, als viele kurze Beispiele ihm geben. Die PHP-Dokumentation warnt, dass das sofortige Löschen alter Session-Daten bei parallelen Anfragen oder instabilen Netzwerken zu verlorenen Sessions oder inkonsistentem Zustand führen kann. Den alten Datensatz unbegrenzt zu behalten, ist ebenfalls unerwünscht. Eine Anwendung mit höherem Risiko oder hoher Parallelität braucht ein ausdrückliches Übergangsmodell: die alte ID als veraltet markieren, nur bei Bedarf eine kurze Überschneidung erlauben, sie danach ablehnen und verdächtige Wiederverwendung protokollieren, ohne die rohe ID zu speichern.
Das kleine Beispiel verwendet false, um nicht zu behaupten, sofortiges Löschen sei immer sicher. Es ist keine vollständige Revocation-Implementierung. Anwendungen mit parallelen AJAX-Anfragen, mehreren Workern oder einem eigenen Redis-/Datenbank-Handler müssen diesen Übergang passend zu ihrem tatsächlichen Speicher- und Nebenläufigkeitsmodell entwerfen und testen.
Ablaufregeln in der Anwendungslogik durchsetzen
Der Cookie-Ablauf steuert, wie lange der Browser ein Cookie aufbewahren darf. Die Garbage Collection von PHP steuert, wann alte serverseitige Datensätze entfernt werden können. Keines von beiden beschreibt für sich allein die Autorisierungsrichtlinie der Anwendung. Das PHP-Handbuch warnt ausdrücklich davor, session.gc_maxlifetime als garantierten Ablaufmechanismus zu behandeln, und empfiehlt eine zeitstempelbasierte Verwaltung der Lebensdauer.
Zwei Fristen beantworten unterschiedliche Fragen:
- Ein Inaktivitäts-Timeout legt fest, wie lange eine authentifizierte Session unbenutzt bleiben darf.
- Ein absoluter Timeout legt das maximale Alter der Session fest, auch wenn sie aktiv bleibt.
$now = time();
$idleLimit = 30 * 60;
$absoluteLimit = 8 * 60 * 60;
$idleExpired = isset($_SESSION['last_activity_at'])
&& $now - $_SESSION['last_activity_at'] > $idleLimit;
$absoluteExpired = isset($_SESSION['authenticated_at'])
&& $now - $_SESSION['authenticated_at'] > $absoluteLimit;
if ($idleExpired || $absoluteExpired) {
// Run the same complete invalidation used by logout, then require login.
} else {
$_SESSION['last_activity_at'] = $now;
}
Die Zahlen veranschaulichen den Mechanismus und sind keine universelle Richtlinie. Ein redaktionelles Dashboard, eine öffentliche Präferenz-Session und eine Zahlungsfreigabe haben unterschiedliche Folgen und Bedienbarkeitskosten. Grenzen sollten aus der Sensibilität der Aktion, dem erwarteten Benutzerverhalten und der Möglichkeit zur erneuten Authentifizierung abgeleitet werden. Zeitstempel gehören auf den Server, sind bei jeder geschützten Anfrage zu prüfen und benötigen eine konsistente Serverzeit.
Die Abmeldung hat drei Ebenen
session_destroy() klingt umfassend, doch das dokumentierte Verhalten ist enger: Die Funktion zerstört die Daten der aktuellen Session, leert aber weder $_SESSION noch löscht sie das Session-Cookie. Eine vollständige Abmeldung muss normalerweise alle drei Ebenen bereinigen.
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'] ?: 'Lax',
]);
}
session_destroy();
Das Lösch-Cookie muss denselben Namen sowie einen kompatiblen Path-/Domain-Geltungsbereich wie das ursprüngliche Cookie verwenden. Andernfalls kann der Browser das ursprüngliche Cookie behalten. Auch parallele Anfragen bleiben eine Einschränkung: Eine laufende Anfrage kann auf Zustand zugreifen, während eine andere die Abmeldung ausführt. Sensible Systeme benötigen möglicherweise serverseitige Revocation-Datensätze oder eine kontoweite Session-Verwaltung, statt nur einen einzelnen Datensatz zu löschen.
Verhalten prüfen, statt der Konfiguration zu vertrauen
Eine Überprüfung ist möglich, ohne die Anwendung anzugreifen:
- Die Login-Antwort in den Browser-Entwicklerwerkzeugen untersuchen. Das Session-Cookie muss
Secure,HttpOnly, den vorgesehenenSameSite-Wert und keine unnötigeDomainbesitzen. - Nur festhalten, ob sich die ID ändert, nicht die ID selbst. Sie sollte sich bei erfolgreicher Anmeldung und weiteren Berechtigungsänderungen erneuern.
- In einer Testumgebung eine erfundene, unbekannte Session-ID senden. Mit strict mode und dem vorgesehenen Handler sollte PHP eine gültige serverseitig erzeugte ID ausgeben oder verwenden, statt die gelieferte zu übernehmen.
- Kontrollierte Test-Zeitstempel verschieben, um Inaktivitäts- und absoluten Ablauf unabhängig zu prüfen.
- Abmelden und anschließend bestätigen, dass das Browser-Cookie entfernt ist und eine Wiederholung des alten Test-Cookies keine geschützte Route erreicht.
- Tests mit parallelen Anfragen rund um Erneuerung und Abmeldung ausführen, wenn die Oberfläche parallele Anfragen erzeugt.
Protokolle sollten Ereignisse wie Erstellung, Erneuerung, Ablaufgrund und Abmeldung erfassen, jedoch keine rohen Session-IDs. Selbst das Hashen einer ID zur Korrelation kann sensible verknüpfbare Daten erzeugen; Aufbewahrung und Zugriff sollten deshalb begrenzt sein.
Was Session-Härtung nicht löst
Eine gut verwaltete Session kann weiterhin die falsche Aktion autorisieren, wenn die Anwendung eine fehlerhafte Zugriffskontrolle besitzt. Sie kann weiterhin von bösartigem Code verwendet werden, der auf der Seite läuft. Sie ersetzt weder Output Encoding, CSRF-Schutz, TLS, Passwortsicherheit und Rate Limiting noch eine erneute Authentifizierung vor besonders sensiblen Aktionen.
Eine starre Bindung der Session an eine IP-Adresse ist ebenfalls kein einfaches Heilmittel. Mobilfunknetze, Proxys und wechselnde Adressen können legitime Benutzer ablehnen, während ein Angreifer über denselben scheinbaren Netzwerkpfad kommen kann. Signale wie IP-Änderungen können die Risikoerkennung unterstützen; sie als Identitätsnachweis zu behandeln, erfordert jedoch sorgfältige Tests.
Eine Session ist ein Prozess, kein Cookie-Flag
Die wichtigste Verbesserung ist keine magische Einzeloption, sondern Konsistenz über den gesamten Lebenszyklus: nur serverseitig erzeugte IDs akzeptieren, sie ausschließlich in eng begrenzten sicheren Cookies übertragen, bei Vertrauensänderungen erneuern, Timeouts auf dem Server durchsetzen und bei der Abmeldung jede Ebene entfernen.
Für Nebenläufigkeit und die Invalidierung alter IDs bleibt eine Entwurfsentscheidung, und der richtige Timeout hängt von der Anwendung ab. Diese Entscheidungen ausdrücklich zu treffen, ist ehrlicher und hilfreicher, als ein „Secure Session“-Snippet zu kopieren und die Arbeit für abgeschlossen zu halten.
