Passwörter gehören zu den sensibelsten Daten jeder Webanwendung. Werden sie im Klartext oder mit veralteten Verfahren wie MD5 gespeichert, reicht ein einziger Datenbankeinbruch aus, um sämtliche Zugangsdaten offenzulegen. PHP stellt mit password_hash(), password_verify() und password_needs_rehash() drei Funktionen bereit, die das sichere Hashen und Verifizieren von Passwörtern erheblich vereinfachen. Dieses Tutorial erklärt die Funktionsweise dieser Funktionen, zeigt den Unterschied zwischen den verfügbaren Algorithmen und demonstriert eine vollständige Implementierung für Registrierung und Login.

Zunächst stellt sich die Frage, warum Passwörter überhaupt gehasht werden müssen und welche Risiken bei falscher Speicherung entstehen.
Warum Passwörter hashen?
Beim Hashing wird ein Passwort durch eine mathematische Einwegfunktion in eine Zeichenkette fester Länge umgewandelt. Im Gegensatz zur Verschlüsselung lässt sich ein Hash nicht zurückrechnen. Selbst der Betreiber der Anwendung kann das ursprüngliche Passwort nicht mehr aus dem Hash ableiten. Wird die Datenbank kompromittiert, erhält ein Angreifer lediglich die Hashes, nicht die Klartextpasswörter. Ohne Hashing wären bei einem Datenbankleck alle Passwörter sofort lesbar. Da viele Nutzer dasselbe Passwort bei mehreren Diensten verwenden, hätte ein solcher Vorfall weitreichende Folgen über die eigene Anwendung hinaus.
Warum MD5 und SHA1 unsicher sind
Obwohl MD5 und SHA1 technisch gesehen Hash-Funktionen sind, eignen sie sich nicht zum Speichern von Passwörtern. Beide Algorithmen wurden für Geschwindigkeit optimiert und erzeugen Milliarden von Hashes pro Sekunde auf moderner Hardware. Ein Angreifer kann dadurch in kurzer Zeit enorme Mengen an Passwortkandidaten durchprobieren. Zusätzlich existieren sogenannte Rainbow Tables, also vorberechnete Tabellen mit Millionen von Klartext-Hash-Paaren. Ein einfacher Abgleich genügt, um gängige Passwörter innerhalb von Sekunden zu entschlüsseln. Algorithmen wie bcrypt und Argon2id sind bewusst langsam konzipiert und verwenden einen individuellen Salt pro Passwort, wodurch Rainbow Tables nutzlos werden und Brute-Force-Angriffe erheblich mehr Zeit benötigen.
<?php
/* MD5 und SHA1: NICHT für Passwörter verwenden! */
$passwort = 'geheim123';
$md5Hash = md5($passwort);
$sha1Hash = sha1($passwort);
/* Beide Hashes lassen sich mit Rainbow Tables leicht knacken */
echo $md5Hash; /* bdc87b9c894da5168059e00ebffb9077 */
echo $sha1Hash; /* 40bd001563085fc35165329ea1ff5c5ecbdbbeef */
password_hash() erklärt
Die Funktion password_hash() ist seit PHP 5.5 verfügbar und der empfohlene Weg, Passwörter in PHP zu hashen. Sie kümmert sich automatisch um die Erzeugung eines kryptographisch sicheren Salts und speichert Algorithmus, Cost-Faktor und Salt gemeinsam im erzeugten Hash-String.
Syntax und Parameter
Der grundlegende Aufruf von password_hash() erwartet zwei Pflichtparameter und einen optionalen dritten Parameter für zusätzliche Optionen.
<?php
/* Syntax: password_hash(string $password, string|int|null $algo, array $options = []): string */
$passwort = 'MeinSicheresPasswort!';
$hash = password_hash($passwort, PASSWORD_DEFAULT);
echo $hash;
/* Beispielausgabe: $2y$10$xN5Rk8kQ3pY1qW4rT7uZ8eJmVnBcXdFgHiJkLmNoPqRsTuVwXyZ */
Der erste Parameter ist das zu hashende Passwort als String. Der zweite Parameter bestimmt den verwendeten Algorithmus. Der optionale dritte Parameter ist ein Array mit Einstellungen wie dem Cost-Faktor. Der erzeugte Hash-String enthält alle Informationen, die später zur Verifikation benötigt werden.
PASSWORD_DEFAULT vs. PASSWORD_BCRYPT vs. PASSWORD_ARGON2ID
PHP bietet mehrere Algorithmus-Konstanten zur Auswahl. Die richtige Wahl hängt von den Anforderungen und der verfügbaren PHP-Version ab.
Das folgende Diagramm zeigt die Entscheidungsfindung bei der Wahl des Algorithmus.
flowchart TD
A["Algorithmus wählen"] --> B{"PHP >= 7.3 und
Argon2 verfügbar?"}
B -- Ja --> C["PASSWORD_ARGON2ID
Modernster Algorithmus"]
B -- Nein --> D{"Maximale
Kompatibilität?"}
D -- Ja --> E["PASSWORD_DEFAULT
Passt sich automatisch an"]
D -- Nein --> F["PASSWORD_BCRYPT
Bewährt und sicher"]
C --> G["Hash beginnt mit $argon2id$"]
E --> H["Hash beginnt aktuell mit $2y$"]
F --> H
PASSWORD_DEFAULT nutzt aktuell bcrypt, kann sich aber in zukünftigen PHP-Versionen ändern, wenn ein stärkerer Algorithmus zum Standard wird. PASSWORD_BCRYPT erzeugt immer einen bcrypt-Hash und ist auf 72 Bytes Passwortlänge begrenzt. PASSWORD_ARGON2ID ist seit PHP 7.3 verfügbar und gilt als der modernste Algorithmus. Er bietet Schutz gegen GPU-basierte Angriffe und Side-Channel-Attacken. Für die meisten Anwendungen ist PASSWORD_DEFAULT die beste Wahl, da der Algorithmus automatisch mit zukünftigen PHP-Versionen aktualisiert wird.
Der automatische Salt
Ein Salt ist eine zufällige Zeichenkette, die dem Passwort vor dem Hashing hinzugefügt wird. Dadurch erzeugt dasselbe Passwort bei jedem Aufruf einen anderen Hash, was Rainbow-Table-Angriffe wirkungslos macht. Die Funktion password_hash() generiert diesen Salt vollautomatisch mit einem kryptographisch sicheren Zufallsgenerator. Der frühere salt-Parameter in den Optionen ist seit PHP 7.0 als veraltet markiert und sollte nicht mehr verwendet werden. Ein manuell erzeugter Salt ist fast immer schwächer als der automatisch generierte.
<?php
$passwort = 'IdentischesPasswort';
/* Jeder Aufruf erzeugt durch den automatischen Salt einen anderen Hash */
$hash1 = password_hash($passwort, PASSWORD_DEFAULT);
$hash2 = password_hash($passwort, PASSWORD_DEFAULT);
var_dump($hash1 === $hash2); /* false */
/* Trotzdem verifizieren beide korrekt */
var_dump(password_verify($passwort, $hash1)); /* true */
var_dump(password_verify($passwort, $hash2)); /* true */
Der Cost-Parameter
Der Cost-Parameter bestimmt, wie rechenintensiv das Hashing ist. Bei bcrypt verdoppelt jede Erhöhung um 1 die benötigte Rechenzeit. Der Standardwert liegt bei 10. Ein höherer Wert erhöht die Sicherheit, verlängert aber auch die Wartezeit beim Login. Ein guter Richtwert ist eine Dauer von etwa 100 Millisekunden pro Hash-Berechnung.
<?php
/* Cost-Parameter anpassen */
$optionen = ['cost' => 12];
$hash = password_hash('MeinPasswort', PASSWORD_BCRYPT, $optionen);
echo $hash;
/* $2y$12$... - die 12 nach $2y$ zeigt den Cost-Faktor */
/* Optimalen Cost-Faktor für den Server ermitteln */
$zielZeit = 0.1; /* 100 Millisekunden */
$cost = 8;
do {
$cost++;
$start = microtime(true);
password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
$dauer = microtime(true) - $start;
} while ($dauer < $zielZeit);
echo 'Empfohlener Cost-Faktor: ' . $cost;
Passwörter verifizieren mit password_verify()
Die Funktion password_verify() vergleicht ein Klartextpasswort mit einem gespeicherten Hash. Sie extrahiert den Algorithmus, den Cost-Faktor und den Salt direkt aus dem Hash-String und berechnet daraus den Vergleichshash. Der Vergleich erfolgt timing-safe, das heißt, die Laufzeit ist unabhängig davon, an welcher Stelle sich die Hashes unterscheiden. Dadurch werden Timing-Angriffe verhindert.
<?php
$gespeicherterHash = '$2y$10$xN5Rk8kQ3pY1qW4rT7uZ8eJmVnBcXdFgHiJkLmNoPqRsTuVwXyZ';
$eingabe = 'MeinSicheresPasswort!';
if (password_verify($eingabe, $gespeicherterHash)) {
echo 'Passwort ist korrekt.';
} else {
echo 'Passwort ist falsch.';
}
/* NIEMALS Hashes direkt mit === vergleichen! */
/* $hash1 === $hash2 funktioniert nicht, da jeder Hash einen eigenen Salt enthält */
Passwörter aktualisieren mit password_needs_rehash()
Wenn der Algorithmus oder der Cost-Faktor geändert wird, müssen bestehende Hashes aktualisiert werden. Die Funktion password_needs_rehash() prüft, ob ein vorhandener Hash noch den aktuellen Einstellungen entspricht. Der ideale Zeitpunkt für diese Prüfung ist direkt nach einem erfolgreichen Login, da zu diesem Zeitpunkt das Klartextpasswort vorliegt.
<?php
$hash = '$2y$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ12345';
$passwort = 'NutzerPasswort';
$optionen = ['cost' => 12];
if (password_verify($passwort, $hash)) {
if (password_needs_rehash($hash, PASSWORD_DEFAULT, $optionen)) {
$neuerHash = password_hash($passwort, PASSWORD_DEFAULT, $optionen);
/* $neuerHash in der Datenbank speichern */
/* UPDATE benutzer SET passwort_hash = ? WHERE id = ? */
}
/* Login erfolgreich */
}
Praxisbeispiel: Registrierung und Login
Das folgende Beispiel zeigt eine vollständige Implementierung für Registrierung und Login mit PDO und einer MySQL-Datenbank. Das Passwortfeld in der Datenbank muss mindestens VARCHAR(255) sein, um auch längere Hashes von Argon2id aufnehmen zu können.
<?php
/* Registrierung: Passwort hashen und speichern */
function registrieren(PDO $db, string $email, string $passwort): bool
{
$hash = password_hash($passwort, PASSWORD_DEFAULT, ['cost' => 12]);
$stmt = $db->prepare('INSERT INTO benutzer (email, passwort_hash) VALUES (?, ?)');
return $stmt->execute([$email, $hash]);
}
/* Login: Passwort verifizieren und Hash bei Bedarf aktualisieren */
function einloggen(PDO $db, string $email, string $passwort): bool
{
$stmt = $db->prepare('SELECT id, passwort_hash FROM benutzer WHERE email = ?');
$stmt->execute([$email]);
$benutzer = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$benutzer) {
/* Timing-Angriffe verhindern: Hash trotzdem berechnen */
password_hash($passwort, PASSWORD_DEFAULT);
return false;
}
if (!password_verify($passwort, $benutzer['passwort_hash'])) {
return false;
}
/* Hash bei Bedarf aktualisieren */
if (password_needs_rehash($benutzer['passwort_hash'], PASSWORD_DEFAULT, ['cost' => 12])) {
$neuerHash = password_hash($passwort, PASSWORD_DEFAULT, ['cost' => 12]);
$update = $db->prepare('UPDATE benutzer SET passwort_hash = ? WHERE id = ?');
$update->execute([$neuerHash, $benutzer['id']]);
}
return true;
}
In der einloggen-Funktion wird bei einem nicht existierenden Benutzer trotzdem ein Hash berechnet. Das verhindert, dass ein Angreifer anhand der Antwortzeit erkennen kann, ob eine E-Mail-Adresse in der Datenbank existiert.
Häufige Fehler und Sicherheitshinweise
Beim Umgang mit Passwort-Hashing gibt es mehrere typische Fehler, die die Sicherheit einer Anwendung gefährden können. Das Passwort sollte vor dem Hashing niemals gekürzt, getrimmt oder anderweitig verändert werden. Funktionen wie strtolower() oder trim() reduzieren die Entropie des Passworts. Ebenso sollte das Passwort nicht vor dem Hashing mit htmlspecialchars() oder addslashes() bearbeitet werden, da password_verify() dann mit dem unbearbeiteten Passwort fehlschlägt. Das Datenbankfeld muss VARCHAR(255) sein, nicht VARCHAR(60), obwohl bcrypt-Hashes aktuell nur 60 Zeichen lang sind. Wird später auf Argon2id umgestellt, benötigt der Hash mehr Platz. Der frühere salt-Parameter in den Optionen ist seit PHP 7.0 veraltet und erzeugt eine Deprecation-Warnung. Die automatische Salt-Erzeugung von password_hash() ist stets die sicherere Variante. Zuletzt ist zu beachten, dass bcrypt Passwörter auf 72 Bytes begrenzt. Alles darüber hinaus wird stillschweigend abgeschnitten. Wer längere Passwörter unterstützen muss, sollte auf PASSWORD_ARGON2ID umsteigen.
Fazit
Die PHP-Funktionen password_hash(), password_verify() und password_needs_rehash() bilden ein vollständiges System zum sicheren Umgang mit Passwörtern. Sie erzeugen automatisch einen kryptographisch sicheren Salt, verwenden bewährte Algorithmen wie bcrypt oder Argon2id und ermöglichen eine nahtlose Aktualisierung bestehender Hashes bei geänderten Sicherheitsanforderungen. Mit PASSWORD_DEFAULT als Algorithmus und einem angemessenen Cost-Faktor sind Passwörter gegen Brute-Force-Angriffe und Rainbow Tables geschützt. Die Kombination aus password_verify() und password_needs_rehash() beim Login-Vorgang stellt sicher, dass die Hashes stets den aktuellen Sicherheitsstandards entsprechen.