Ein Login ist erst dann sauber abgeschlossen, wenn auch die Session-ID gewechselt hat. In PHP erledigt das eine einzige Funktion, die in vielen Projekten trotzdem fehlt. Dieses Tutorial zeigt, warum dieser Aufruf so wichtig ist, an welche Stellen im Code er gehört und worauf bei parallelen Requests zu achten ist.
Was macht php session_regenerate_id()?
Wer in einer PHP-Anwendung mit Logins arbeitet, stösst früher oder später auf die Funktion PHP session_regenerate_id. Sie tauscht die aktuelle session_id gegen eine frisch erzeugte neue Session-ID aus, ohne die in $_SESSION gespeicherten Daten zu verwerfen. Im Browser landet ein neuer Cookie, der nächste Request läuft mit der neuen ID auf die neue Session zu, die alte ID verliert den Zugriff.

Bevor der Login-Flow im Detail folgt, lohnt sich ein Blick darauf, welcher Angriff durch diesen einen Funktionsaufruf wirksam abgewehrt wird.
Genau dieser Tausch ist der wichtigste Schutz gegen einen Klassiker der Webangriffe: Session-Fixation. Ohne den Aufruf bleibt nach dem Login die gleiche Session-ID gültig, die der Angreifer dem Opfer untergeschoben hat. Mit dem Aufruf wird die ID erneuert, der Angreifer steht plötzlich draußen. PHP session_regenerate_id ist damit ein winziger, aber unverzichtbarer Baustein in jedem Login-Flow.
Was ist Session-Fixation?
Bei Session-Fixation legt der Angreifer dem Opfer eine bekannte Session-ID nahe, beispielsweise über einen präparierten Link. Loggt sich das Opfer in dieser Session ein, hat der Angreifer dieselbe ID und kann sich mit ihr beim Server als das Opfer ausgeben. Das funktioniert, weil PHP standardmäßig die Session-ID über den Login hinweg behält.
Mit php session_regenerate_id wird genau diese Linie zerschnitten. Sobald die Funktion nach erfolgreichem Login aufgerufen wird, ist die alte ID Geschichte und der Angreifer hält nur noch eine ungültige Zeichenkette in der Hand. Wer das Konzept noch tiefer verstehen will, kann das Tutorial zu session_start() und Session-Sicherheit lesen.
Aufruf nach erfolgreichem Login
Der wichtigste Aufrufpunkt ist der Moment direkt nach erfolgreicher Passwort-Prüfung. Bevor irgendwelche User-Daten in der Session landen, wird die Session-ID rotiert. Damit ist die Session ab Login eindeutig dem authentifizierten Nutzer zugeordnet. Das folgende Beispiel zeigt den klassischen Login-Flow mit anschließender ID-Rotation.
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim((string) $_POST['email']);
$pass = (string) $_POST['password'];
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row !== false && password_verify($pass, $row['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = (int) $row['id'];
$_SESSION['login_time'] = time();
header('Location: /dashboard.php');
exit;
}
}
Der Aufruf von password_verify() ist hier kombiniert mit php session_regenerate_id. Mehr über die sichere Passwort-Prüfung steht im Tutorial zu PHP password_hash und password_verify.
Aufruf bei Privilege-Wechsel und sensiblen Aktionen
Nicht nur der Login ist ein guter Zeitpunkt für eine Rotation. Immer wenn sich die Berechtigung des Nutzers ändert, sollte eine neue Session-ID vergeben werden. Klassiker sind die Aktivierung eines Admin-Modus, die Annahme einer 2FA-Challenge oder das Bestätigen eines Account-Wechsels.
<?php
session_start();
if ($_SESSION['rolle'] === 'user' && $neueRolle === 'admin') {
session_regenerate_id(true);
$_SESSION['rolle'] = 'admin';
}
Dieselbe Logik gilt für sensitive Aktionen wie Passwort-Reset oder Änderung einer hinterlegten E-Mail-Adresse. Das Risiko, dass eine veraltete Session-ID in falsche Hände gelangt ist, wird durch jede Rotation kleiner. Ein blinder Aufruf in jedem Request kostet aber unnötig Performance, daher ist die Empfehlung: nach Login obligatorisch, bei Privilege-Wechsel sehr empfohlen, sonst nur in besonderen Fällen.
Der Parameter delete_old_session
Die Funktionssignatur sieht harmlos aus, hat aber einen wichtigen Schalter: delete_old_session. Ist der Wert true, wird die alte Session-Datei sofort beim Aufruf gelöscht. Ist er false (Default), bleibt die alte Session noch kurz parallel verfügbar, der Browser hat aber bereits den neuen Cookie erhalten.
<?php
/* harte Variante: alte Session weg */
session_regenerate_id(true);
/* sanfte Variante: alte Session bleibt noch einige Sekunden gueltig */
session_regenerate_id(false);
Welche Variante richtig ist, hängt vom Kontext ab. Beim Login spricht alles für true: die alte Session war zwar leer, aber sicherheitstechnisch klarer ist die sofortige Löschung. Bei laufenden Anwendungen mit AJAX-Requests im Hintergrund ist false oft die robustere Wahl, weil der laufende AJAX-Request noch mit der alten ID beim Server ankommt und diese kurzfristig anerkannt wird.
Begleitende Cookie-Konfiguration (HttpOnly, Secure, SameSite)
php session_regenerate_id alleine schützt nur vor Session-Fixation. Gegen Session-Hijacking, also den Diebstahl einer aktiven Session, helfen zusätzliche Cookie-Flags. Sie werden vor dem ersten session_start() über session_set_cookie_params() gesetzt.
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
HttpOnly verhindert, dass JavaScript den Cookie auslesen kann, was XSS-Angriffe deutlich entschärft. Secure sorgt dafür, dass der Cookie nur über HTTPS gesendet wird. SameSite=Lax blockiert Cross-Site-Cookies in den meisten Fällen und erschwert CSRF. Mehr zu den Cookie-Flags im Tutorial PHP setcookie() von A bis Z.
Race-Conditions bei AJAX-Requests
In modernen Anwendungen laufen oft mehrere AJAX-Requests gleichzeitig. Wird mitten in dieser Welle php session_regenerate_id mit delete_old_session=true aufgerufen, kann es passieren, dass ein paralleler Request mit der bereits ungültigen Session-ID am Server eintrifft. Das führt dann zu einem unerwarteten Logout oder zu Datenverlust.
<?php
session_start();
/* ... laufende Logik ... */
session_regenerate_id(false);
session_write_close();
/* alte Session bleibt kurz erhalten, Lock wird sofort freigegeben */
Mit delete_old_session=false und einem schnell folgenden session_write_close() wird das Risiko minimal. Wer sicher sein will, kann zusätzlich Frontend-seitig nach einem Login warten, bis die nächste interaktive Aktion startet.
Der folgende Ablauf zeigt den Angriff und die Wirkung der Rotation im direkten Vergleich.
sequenceDiagram
participant A as Angreifer
participant O as Opfer
participant S as Server
A->>O: Link mit fester Session-ID
O->>S: Loggt sich ein (alte ID)
Note over S: Ohne Rotation hat A Zugriff
S->>O: Mit Rotation: neue Session-ID
A--xS: Alte ID nicht mehr gueltig
Periodische Rotation als Defense-in-Depth
Die Pflicht-Aufrufe nach Login und Privilege-Wechsel sind das Minimum. Für besonders sensible Anwendungen wie Online-Banking oder Admin-Backends lohnt sich eine zusätzliche periodische Rotation, etwa alle 15 bis 30 Minuten. Damit wird das Zeitfenster für einen gestohlenen Cookie automatisch klein gehalten.
<?php
session_start();
$intervall = 1800; /* 30 Minuten */
$jetzt = time();
$letzte = $_SESSION['last_regenerate'] ?? 0;
if ($jetzt - $letzte > $intervall) {
session_regenerate_id(true);
$_SESSION['last_regenerate'] = $jetzt;
}
Diese Variante reduziert das Hijacking-Risiko, ohne den Login-Flow umzubauen. Der Wert wird in der Session selbst hinterlegt und wächst transparent mit jeder Anfrage mit. Beim ersten Request nach Ablauf des Intervalls wird die ID rotiert und der Zähler zurückgesetzt. Wer mit session_id() den aktuellen Wert abfragen muss, etwa um ihn an einen WebSocket-Server weiterzureichen, sollte das ausschliesslich serverseitig tun und niemals an den JavaScript-Client zurückspielen, weil sonst der HttpOnly-Schutz durchbrochen wäre.
Custom Session Handler und ID-Rotation
php session_regenerate_id arbeitet unabhängig vom Storage-Backend. Wer einen eigenen SessionHandlerInterface registriert, etwa für Sessions in Redis, MySQL oder Memcached, kann die Funktion ganz normal nutzen. Intern ruft PHP die Methoden read, write und destroy des Handlers auf und erzeugt dabei den neuen Datensatz mit der frischen ID.
<?php
class RedisSessionHandler implements SessionHandlerInterface
{
/* open, close, read, write, destroy, gc - alle implementieren */
}
session_set_save_handler(new RedisSessionHandler(), true);
session_start();
/* funktioniert wie gewohnt, ID-Rotation laeuft transparent */
session_regenerate_id(true);
Wichtig ist nur, dass die destroy-Methode wirklich den alten Datensatz löscht, wenn delete_old_session=true übergeben wurde. Bleibt die alte ID im Storage stehen, bringt die Rotation sicherheitstechnisch nichts. Ein kurzer Unit-Test, der nach session_regenerate_id(true) prüft, ob die alte ID im Backend wirklich verschwunden ist, lohnt sich.
Session-Storage und ID-Rotation
Die Frage "Ich speichere meine Sessions in Dateien auf dem Server, muss ich die ID dann überhaupt regenerieren?" taucht immer wieder auf. Die kurze Antwort: ja, immer. Die ID ist das Token, das im Browser-Cookie steht und vom Angreifer abgefangen oder vorbestimmt werden kann. Der Storage (Datei, DB, Redis, Memcached) ist davon getrennt und schaltet die Angriffsvektoren nicht ab.
Egal ob die Sessions im Standard-Verzeichnis von PHP liegen, in einer Tabelle sessions der Datenbank oder in einem geclusterten Redis: Session-Fixation und Session-Hijacking funktionieren immer über die ID im Cookie. Erst der Tausch dieser ID nach Login und Privilege-Wechsel macht die Angriffe wirkungslos. Der Storage entscheidet nur darüber, wo die Daten landen, und nicht darüber, wer Zugriff bekommt.
Fazit
php session_regenerate_id ist eine kleine Funktion mit großer Wirkung. Wer sie konsequent nach jedem Login und bei jedem Privilege-Wechsel aufruft, schließt die Tür für Session-Fixation-Angriffe nahezu vollständig. Der Parameter delete_old_session braucht eine bewusste Entscheidung: harte Variante für den Login, sanfte Variante für Anwendungen mit parallelen AJAX-Requests. In Kombination mit HttpOnly, Secure und SameSite entsteht eine Session-Schicht, die einem realistischen Angriff Stand hält. Damit ist php session_regenerate_id einer dieser One-Liner, deren Abwesenheit Jahre später im Penetrationstest auffällt und immer einen unangenehmen Bug-Report nach sich zieht.