Sichere Sessions und Anmeldung
Nach der Anmeldung hält die Session den Benutzer auf der Website angemeldet – und wer ihre Kennung bekommt, ist an seiner Stelle angemeldet. Wie Sie das Session-Cookie einstellen, was bei Anmeldung und Abmeldung zu tun ist und wie Sie das Erraten von Passwörtern begrenzen.
Die Beispiele verwenden die Verbindung $pdo aus dem Artikel über PDO und die Tabelle users aus dem Artikel über das Speichern von Passwörtern.
Einstellungen des Session-Cookies
session_set_cookie_params([
"lifetime" => 0,
"path" => "/",
"secure" => true,
"httponly" => true,
"samesite" => "Lax",
]);
ini_set("session.use_strict_mode", "1");
session_start();
- httponly – JavaScript kann das Cookie nicht lesen. Eine mögliche XSS-Lücke kann die Session-Kennung daher nicht stehlen.
- secure – das Cookie wird nur über HTTPS gesendet. Die ganze Website muss deshalb über HTTPS laufen.
- samesite – der Wert Lax verhindert, dass das Cookie bei POST-Anfragen von anderen Websites gesendet wird, was zusätzlicher Schutz vor CSRF ist. Der Wert Strict würde das Cookie nicht einmal senden, wenn der Benutzer über einen Link von einer anderen Website kommt, und nach dem Klick auf den Link wäre er scheinbar abgemeldet.
- lifetime 0 – das Cookie gilt, bis der Browser geschlossen wird.
- use_strict_mode – PHP akzeptiert keine Session-Kennung, die es nicht selbst erzeugt hat. Ein Angreifer kann dem Benutzer also keine im Voraus bekannte Kennung unterschieben.
Die Einstellungen als Array zu übergeben funktioniert seit PHP 7.3. Sie müssen vor session_start und vor jeder Ausgabe der Seite erfolgen.
Anmeldung
function log_in(PDO $pdo, string $email, string $password): bool
{
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if(!$user || !password_verify($password, $user["password"])) return false;
session_regenerate_id(true);
$_SESSION["user_id"] = (int)$user["id"];
return true;
}
session_regenerate_id(true) vergibt nach der Anmeldung eine neue Session-Kennung und verwirft die alte. Selbst wenn ein Angreifer die Kennung von vor der Anmeldung kannte, ist sie jetzt nutzlos. Gehen Sie genauso vor, wenn sich Rechte ändern. Speichern Sie in der Session nur die ID des Benutzers, Benutzerdaten und Rechte laden Sie aus der Datenbank.
Abmeldung
$_SESSION = []; $params = session_get_cookie_params(); setcookie(session_name(), "", [ "expires" => time() - 3600, "path" => $params["path"], "domain" => $params["domain"], "secure" => $params["secure"], "httponly" => $params["httponly"], "samesite" => $params["samesite"], ]); session_destroy();
Die Abmeldung löscht die Session-Daten auf dem Server und das Cookie im Browser. Setzen Sie die Schaltfläche zum Abmelden in ein Formular, das per POST mit einem CSRF-Token abgesendet wird.
Anmeldeversuche begrenzen
Ohne Begrenzung kann ein Angreifer Passwörter automatisch ausprobieren, tausende pro Minute. Fehlgeschlagene Versuche werden deshalb in eine Tabelle geschrieben:
CREATE TABLE login_attempts ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(45) NOT NULL, attempted_at DATETIME NOT NULL, KEY idx_ip_attempted_at (ip, attempted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Nach fünf fehlgeschlagenen Versuchen von einer IP-Adresse innerhalb von 15 Minuten werden weitere Anmeldungen abgelehnt:
function too_many_attempts(PDO $pdo, string $ip): bool
{
$stmt = $pdo->prepare("SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at > NOW() - INTERVAL 15 MINUTE");
$stmt->execute([$ip]);
return (int)$stmt->fetchColumn() >= 5;
}
function log_failed_attempt(PDO $pdo, string $ip): void
{
$stmt = $pdo->prepare("INSERT INTO login_attempts (ip, attempted_at) VALUES (?, NOW())");
$stmt->execute([$ip]);
}
// Verarbeitung des Anmeldeformulars (nach der Prüfung des CSRF-Tokens)
$email = is_string($_POST["email"] ?? null) ? trim($_POST["email"]) : "";
$password = is_string($_POST["password"] ?? null) ? $_POST["password"] : "";
$ip = $_SERVER["REMOTE_ADDR"];
if(too_many_attempts($pdo, $ip))
{
$error = "Zu viele Versuche. Bitte versuchen Sie es in 15 Minuten erneut.";
}
elseif(log_in($pdo, $email, $password))
{
header("Location: /admin/");
exit;
}
else
{
log_failed_attempt($pdo, $ip);
$error = "E-Mail-Adresse oder Passwort ist falsch.";
}
- Gespeichert werden nur die IP-Adresse und die Zeit. Die eingegebene E-Mail-Adresse und das Passwort werden nirgends gespeichert.
- Die Spalte ip hat 45 Zeichen, damit auch eine IPv6-Adresse hineinpasst.
- Löschen Sie alte Einträge regelmäßig, zum Beispiel einmal am Tag: DELETE FROM login_attempts WHERE attempted_at < NOW() - INTERVAL 1 DAY.
- Läuft die Website hinter einem Reverse Proxy oder einem CDN, enthält REMOTE_ADDR die Adresse des Proxys und die Begrenzung würde für alle Besucher zugleich gelten. Die echte Adresse wird dann in einem Header vom Proxy übergeben – vertrauen dürfen Sie ihr nur, wenn die Anfrage wirklich über Ihren Proxy kam.
Weitere Grundsätze
- Betreiben Sie die ganze Website über HTTPS und richten Sie die Umleitung von HTTP auf dem Server ein.
- Speichern Sie Passwörter mit password_hash, schützen Sie Formulare mit einem CSRF-Token und greifen Sie auf die Datenbank nur über Prepared Statements zu.
- Die Administration können Sie mit einer zusätzlichen Schicht schützen, etwa durch eine Beschränkung des Zugriffs nach IP-Adresse.
Verwandte Artikel:
Passwörter speichern mit password_hash
Formulare gegen CSRF schützen
Prepared Statements in PDO