von MD5-Fallen bis zum sicheren Rehash-on-Login
Wer Passwörter mit MD5 oder SHA-1 hasht, öffnet Angreifern nach einem Datenbank-Leak Tür und Tor für Brute-Force und Rainbow-Table-Angriffe. Dieser Artikel erklärt, wie bcrypt und Argon2id funktionieren, wie ihr sie mit PHP's password_hash() korrekt einsetzt und welche Fehler bei Salt, Cost-Faktor und Rehashing in der Praxis am häufigsten passieren.
Inhaltsverzeichnis
- 1. Warum MD5 und SHA-1 für Passwort-Hashing ungeeignet sind
- 2. Salt: warum jedes Passwort einen eigenen Zufallswert braucht
- 3. Work-Factor und Cost-Faktor: Tuning gegen schnellere Hardware
- 4. bcrypt im Detail: Blowfish, Cost-Faktor und Reife
- 5. Argon2 und Argon2id: Memory-Hardness gegen GPU/ASIC-Angriffe
- 6. PHP's password_hash() und password_verify() richtig einsetzen
- 7. Rehash-on-Login mit password_needs_rehash()
- 8. Häufige Fehler: Verschlüsselung, fehlende Salts, Truncation
- 9. Hashing-Algorithmen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum MD5 und SHA-1 für Passwort-Hashing ungeeignet sind
MD5 und SHA-1 sind kryptographische Hashfunktionen, die für Integritätsprüfung und Checksummen entwickelt wurden, nicht für die Speicherung von Passwörtern. Ihr zentrales Designziel ist Geschwindigkeit: möglichst viele Daten pro Sekunde verarbeiten, um Datei-Prüfsummen oder Signaturen effizient zu berechnen. Genau diese Eigenschaft, die für Checksummen ein Vorteil ist, macht sie für Passwörter zu einer Schwachstelle, sobald eine Datenbank mit Hashes in die Hände eines Angreifers gerät.
Moderne Grafikkarten berechnen Milliarden von MD5-Hashes pro Sekunde parallel, weil sich die Berechnung trivial auf tausende GPU-Kerne verteilen lässt. Ein Angreifer mit einer gestohlenen Datenbank voller MD5-Hashes kann Milliarden Passwortkandidaten pro Sekunde durchprobieren (Brute-Force) oder vorab berechnete Rainbow-Tables nutzen, um unsalted Hashes innerhalb von Sekunden bis Minuten zurückzuführen. Bei einem typischen achtstelligen Passwort mit gemischten Zeichen ist das kein theoretisches Risiko, sondern eine Frage von Stunden.
Reale Breaches wie der LinkedIn-Vorfall von 2012 mit unsalted SHA-1-Hashes zeigen, wie schnell Millionen Passwörter nach einem Leak im Klartext vorlagen. Purpose-built langsame Hashfunktionen wie bcrypt und Argon2id sind absichtlich so konstruiert, dass sie mehrere Größenordnungen langsamer sind als MD5 oder SHA-1, wodurch Brute-Force-Angriffe wirtschaftlich unattraktiv werden.
<?php
declare(strict_types=1);
// BROKEN: fast hash, no salt, trivially crackable with a modern GPU
function hashPasswordLegacy(string $password): string
{
return md5($password);
}
function verifyPasswordLegacy(string $password, string $hash): bool
{
// Also vulnerable to timing attacks (non-constant-time comparison)
return md5($password) === $hash;
}
// FIXED: purpose-built slow hash with automatic per-user salt
function hashPasswordSecure(string $password): string
{
return password_hash($password, PASSWORD_ARGON2ID);
}
function verifyPasswordSecure(string $password, string $hash): bool
{
// Constant-time comparison built into password_verify()
return password_verify($password, $hash);
}
2. Salt: warum jedes Passwort einen eigenen Zufallswert braucht
Ein Salt ist ein zufälliger Wert, der vor der Hashberechnung mit dem Passwort verknüpft wird. Er sorgt dafür, dass zwei Nutzer mit identischem Passwort unterschiedliche gespeicherte Hashes erhalten. Ohne Salt würde ein Angreifer sofort erkennen, welche Nutzer dasselbe Passwort verwenden, und eine einzige vorberechnete Tabelle würde ausreichen, um alle betroffenen Konten gleichzeitig zu kompromittieren.
Ein globaler, für alle Nutzer identischer Salt reicht nicht aus. Er verhindert zwar generische, öffentlich verfügbare Rainbow-Tables, aber ein gezielter Angreifer kann für genau diesen einen bekannten Salt eine spezifische Tabelle vorberechnen und damit alle Konten auf einmal angreifen. Erst ein pro Nutzer zufälliger Salt macht die Vorberechnung wirtschaftlich sinnlos, weil für jeden einzelnen Hash eine eigene, aufwendige Tabelle nötig wäre. Der Aufwand für den Angreifer skaliert dann linear mit der Anzahl der Nutzer, statt einmalig für die gesamte Datenbank anzufallen.
Wichtig zu verstehen: Ein Salt muss nicht geheim sein, nur einzigartig und aus einer kryptographisch sicheren Zufallsquelle stammen, üblicherweise mindestens 16 Byte. PHP's password_hash() generiert den Salt automatisch mit einer sicheren Quelle und bettet ihn direkt lesbar in den zurückgegebenen Hash-String ein, sodass Entwickler ihn niemals manuell verwalten müssen.
3. Work-Factor und Cost-Faktor: Tuning gegen schnellere Hardware
Adaptive Hashfunktionen wie bcrypt und Argon2 besitzen einen einstellbaren Kostenfaktor, der die Rechenzeit pro Hash künstlich erhöht. Bei bcrypt wächst der Rechenaufwand exponentiell mit dem Cost-Faktor, weil die interne Blowfish-Rundenfunktion 2 hoch Cost-Faktor mal wiederholt wird. Ein Sprung von Cost-Faktor 10 auf 11 verdoppelt also die Rechenzeit, ein Sprung auf 12 vervierfacht sie gegenüber dem Ausgangswert.
Ziel ist üblicherweise eine Hash-Berechnung von etwa 100 bis 500 Millisekunden auf typischer Produktionshardware: lang genug, um Brute-Force-Angriffe massiv zu verlangsamen, kurz genug, um die Login-Latenz für echte Nutzer nicht spürbar zu beeinträchtigen. Dieser Wert muss regelmäßig neu kalibriert werden, weil Serverhardware und CPU-Leistung sich ständig weiterentwickeln.
Der Cost-Faktor muss periodisch angehoben werden, da verfügbare Rechenleistung, insbesondere auf GPU- und Cloud-Instanzen, kontinuierlich zunimmt. Ein Cost-Faktor, der 2015 als sicher galt, kann 2026 unzureichend sein. Die Kombination aus Rehash-on-Login (siehe Abschnitt 7) und regelmäßiger Cost-Faktor-Erhöhung hält bestehende Nutzerkonten automatisch auf einem aktuellen Sicherheitsniveau, ohne dass alle Nutzer ihr Passwort zurücksetzen müssen.
-- INSECURE: plaintext or weakly hashed password with a fixed global salt column
CREATE TABLE customer_account_insecure (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_plain VARCHAR(255) NOT NULL, -- never store reversible text
global_salt VARCHAR(32) NOT NULL -- same value for every row
);
-- CORRECT: a single column stores the self-describing password_hash() output
-- (algorithm, cost factor, and per-row salt are all embedded in the string)
CREATE TABLE customer_account_secure (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL, -- output of password_hash()
password_hash_updated_at DATETIME NULL -- track last rehash for audits
);
4. bcrypt im Detail: Blowfish, Cost-Faktor und Reife
bcrypt basiert auf einer modifizierten Version der Blowfish-Blockchiffre, genannt Eksblowfish ("expensive key schedule Blowfish"), vorgestellt 1999 von Niels Provos und David Mazières. Der eigentlich für Verschlüsselung gedachte, rechenintensive Schlüsselaufbau von Blowfish wird bei bcrypt absichtlich mehrfach wiederholt, um die Hashberechnung gezielt zu verlangsamen, statt sie zu beschleunigen.
Das Format eines bcrypt-Hashes, etwa $2y$12$N9qo8uLOickgx2ZMRZoMye..., enthält Algorithmus-Version, Cost-Faktor und Salt in einem einzigen selbstbeschreibenden String. password_verify() erkennt diese Parameter automatisch beim Vergleich. Eine wichtige Einschränkung: bcrypt verarbeitet nur die ersten 72 Byte des Eingabepassworts, alles danach wird stillschweigend ignoriert, was bei sehr langen Passphrasen zu Entropieverlust führen kann.
bcrypt ist seit über 25 Jahren im praktischen Einsatz, in praktisch jeder Programmiersprache und jedem Framework verfügbar und gut auditiert, ohne bekannte praktische Schwächen bei korrekter Cost-Faktor-Wahl. Diese Reife und Verbreitung ist der Hauptgrund, warum bcrypt weiterhin eine solide Standardwahl ist, auch wenn Argon2id in vielen Aspekten technisch überlegen ist.
5. Argon2 und Argon2id: Memory-Hardness gegen GPU/ASIC-Angriffe
Argon2 gewann 2015 die Password Hashing Competition, einen offenen, mehrjährigen Wettbewerb zur Kür der nächsten Generation von Passwort-Hashfunktionen. Es gibt drei Varianten: Argon2d bietet maximale Resistenz gegen GPU-Cracking, ist aber anfällig für bestimmte Seitenkanal-Angriffe; Argon2i ist seitenkanalresistent und für Passwort-Hashing konzipiert; Argon2id ist ein Hybrid aus beiden und seit RFC 9106 die empfohlene Standardwahl für die meisten Anwendungsfälle.
Im Gegensatz zu bcrypt, das primär CPU-Zeit verbraucht, erzwingt Argon2id zusätzlich die Allokation eines konfigurierbaren Speicherblocks pro Hash-Berechnung. Diese Memory-Hardness ist der entscheidende Vorteil gegenüber bcrypt: GPUs und spezialisierte ASICs besitzen zwar tausende Rechenkerne, aber nur begrenzten schnellen Speicher pro Kern. Ein hoher Speicherbedarf pro Hash macht massive Parallelisierung auf solcher Hardware wirtschaftlich unattraktiv, weil der Speicher zum limitierenden Faktor wird statt der reinen Rechenleistung.
Argon2id nutzt drei zentrale Parameter: memory_cost (Speicher in KB, z.B. 65536 für 64 MB), time_cost (Anzahl der Iterationen) und threads (Grad der Parallelität). OWASP empfiehlt als Minimum für interaktive Logins etwa m=19456 (19 MiB), t=2, p=1, wobei höhere Werte auf ausreichend dimensionierten Servern die Sicherheit weiter erhöhen.
<?php
declare(strict_types=1);
// Argon2id with explicit, tunable cost parameters (OWASP-aligned minimums)
$options = [
'memory_cost' => 19456, // 19 MiB of memory per hash computation
'time_cost' => 2, // number of iterations
'threads' => 1, // degree of parallelism
];
$hash = password_hash($plainPassword, PASSWORD_ARGON2ID, $options);
// Resulting hash is self-describing, e.g.:
// $argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$hash...
// password_verify() reads memory_cost/time_cost/threads back out automatically
$isValid = password_verify($plainPassword, $hash);
6. PHP's password_hash() und password_verify() richtig einsetzen
Seit PHP 5.5 bietet die Password Hashing API einen sicheren Standardweg, der Algorithmus, Salt-Generierung und Cost-Faktor vollständig kapselt. password_hash($password, PASSWORD_BCRYPT) oder PASSWORD_ARGON2ID liefert einen vollständig selbstbeschreibenden String zurück, der direkt in einer VARCHAR(255)-Spalte gespeichert werden kann, ohne dass Salt oder Cost-Faktor separat verwaltet werden müssen.
password_verify($password, $hash) übernimmt den Vergleich vollständig, inklusive Extraktion von Algorithmus, Salt und Cost-Faktor aus dem gespeicherten Hash, und führt den Vergleich in konstanter Zeit durch, was Timing-Angriffe auf die Vergleichslogik verhindert. Ein frisch berechneter Hash darf niemals mit == oder === gegen den gespeicherten Hash verglichen werden, weder bei bcrypt noch bei Argon2, da jeder Aufruf von password_hash() aufgrund des zufälligen Salts einen anderen String erzeugt.
PASSWORD_DEFAULT verweist in aktuellen PHP-Versionen auf bcrypt, kann sich aber in zukünftigen Versionen ändern, etwa zu Argon2id, sobald dessen Verbreitung und Performance-Charakteristika das rechtfertigen. Deshalb bevorzugen viele produktive Codebasen explizite Konstanten wie PASSWORD_ARGON2ID, wenn ein bestimmter Algorithmus verbindlich garantiert werden soll, statt sich auf den sich wandelnden Default zu verlassen.
<?php
declare(strict_types=1);
final class PasswordAuthenticator
{
/**
* Registers a new user by storing a securely hashed password.
*/
public function register(string $email, string $plainPassword): void
{
$hash = password_hash($plainPassword, PASSWORD_ARGON2ID);
$this->userRepository->save($email, $hash);
}
/**
* Verifies login credentials without ever comparing raw strings.
*/
public function login(string $email, string $plainPassword): bool
{
$storedHash = $this->userRepository->findHashByEmail($email);
if ($storedHash === null) {
// Still run password_verify() against a dummy hash to avoid
// leaking account existence through response timing differences
password_verify($plainPassword, '$argon2id$v=19$m=19456,t=2,p=1$dummysaltdummysalt$dummy');
return false;
}
return password_verify($plainPassword, $storedHash);
}
}
7. Rehash-on-Login mit password_needs_rehash()
Weil Passwort-Hashes irreversibel sind, kann ein bestehender Hash nicht nachträglich auf einen höheren Cost-Faktor oder einen neuen Algorithmus migriert werden, ohne das Klartext-Passwort zu kennen. Der einzige Zeitpunkt, an dem das Klartext-Passwort im System verfügbar ist, ist der erfolgreiche Login-Vorgang selbst. Genau diesen Moment nutzt das Rehash-on-Login-Muster gezielt aus.
password_needs_rehash($hash, $algorithm, $options) prüft, ob der gespeicherte Hash mit den aktuell gewünschten Parametern übereinstimmt, und gibt true zurück, wenn ein Rehash nötig ist, etwa nach Erhöhung des Cost-Faktors oder einem Wechsel von bcrypt zu Argon2id. Dieser Check gehört unmittelbar nach jedem erfolgreichen password_verify()-Aufruf in den Login-Flow, denn nur dort ist das Klartext-Passwort noch im Speicher vorhanden.
Für die Migration von Legacy-Systemen mit MD5, SHA-1 oder eigener Verschlüsselung auf password_hash() eignet sich derselbe Mechanismus: Beim ersten erfolgreichen Login mit dem alten Verfahren wird das Passwort sofort mit password_hash() neu gehasht und der alte Hash überschrieben. So verschwinden unsichere Alt-Hashes graduell aus der Datenbank, während aktive Nutzer sich einloggen, ganz ohne erzwungenen Passwort-Reset für die gesamte Nutzerbasis.
<?php
declare(strict_types=1);
namespace Mironsoft\Security\Plugin;
use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\Data\CustomerInterface;
/**
* Plugin that transparently upgrades stale password hashes after a
* successful authentication, without forcing a password reset.
*/
final class RehashCustomerPasswordPlugin
{
private const CURRENT_ALGORITHM = PASSWORD_ARGON2ID;
private const CURRENT_OPTIONS = ['memory_cost' => 19456, 'time_cost' => 2, 'threads' => 1];
public function __construct(
private readonly \Mironsoft\Security\Model\PasswordHashRepository $hashRepository
) {
}
/**
* Runs after Magento's own authentication succeeds and rehashes the
* password if the stored hash no longer matches current parameters.
*/
public function afterAuthenticate(
AccountManagementInterface $subject,
CustomerInterface $result,
string $username,
string $password
): CustomerInterface {
$currentHash = $this->hashRepository->getHashByCustomerId((int) $result->getId());
if (password_needs_rehash($currentHash, self::CURRENT_ALGORITHM, self::CURRENT_OPTIONS)) {
$newHash = password_hash($password, self::CURRENT_ALGORITHM, self::CURRENT_OPTIONS);
$this->hashRepository->updateHash((int) $result->getId(), $newHash);
}
return $result;
}
}
8. Häufige Fehler: Verschlüsselung, fehlende Salts, Truncation
Verschlüsselung statt Hashing ist ein fundamentaler Fehler: AES oder eine andere reversible Verschlüsselung für Passwörter zu verwenden, bedeutet, dass jeder mit Zugriff auf den Schlüssel alle Passwörter im Klartext wiederherstellen kann. Sobald der Schlüssel kompromittiert wird, etwa durch einen Code-Leak, ein unverschlüsseltes Backup oder einen Insider mit Zugriff auf die Konfiguration, sind alle Passwörter sofort offengelegt. Hashing ist bewusst eine Einbahnstraße; es gibt keinen legitimen Grund, ein gespeichertes Nutzerpasswort jemals im Klartext zu benötigen.
Fehlende oder schwache Salts sind der zweithäufigste Fehler. Ein selbstgeschriebener Ansatz wie md5($password . 'firmenname') nutzt einen statischen, für alle Nutzer identischen und leicht erratbaren Salt, der praktisch keinen Schutz gegen gezielte Rainbow-Tables bietet. Ebenso problematisch ist ein Salt aus einer nicht-kryptographischen Zufallsquelle wie rand() oder mt_rand() statt random_bytes(), da solche Werte unter Umständen vorhersehbar sind.
Passwort-Truncation betrifft speziell bcrypt: Es ignoriert stillschweigend alles nach 72 Byte, was bei sehr langen Passphrasen effektiv Entropie verschenkt und in seltenen Fällen zu Kollisionen bei Passwörtern mit identischem 72-Byte-Präfix führen kann. Argon2id hat dieses Limit praktisch nicht. Wer sehr lange Passphrasen unterstützen möchte, sollte entweder Argon2id einsetzen oder eine maximale Passwortlänge bei der Registrierung klar dokumentieren, statt sich auf ein stillschweigendes Abschneiden zu verlassen.
9. Hashing-Algorithmen im direkten Vergleich
Die Wahl des Hashing-Algorithmus hat direkte Konsequenzen für die Widerstandsfähigkeit gegen Offline-Angriffe nach einem Datenbank-Leak. Die folgende Übersicht vergleicht die gängigsten Optionen anhand von geschätzter GPU-Cracking-Geschwindigkeit, eingebautem Salt und Speicherhärte.
| Algorithmus | GPU-Geschwindigkeit (ca.) | Eingebauter Salt | Speicherhärte | Empfehlung |
|---|---|---|---|---|
| MD5 | Mehrere Mrd. Hashes/s | Nein | Nein | Nicht verwenden |
| SHA-1 | Mehrere Mrd. Hashes/s | Nein | Nein | Nicht verwenden |
| SHA-256 (unsalted) | Mehrere Mrd. Hashes/s | Nein | Nein | Nicht für Passwörter |
| bcrypt (Cost 12) | Wenige Tausend Hashes/s | Ja | Nein (CPU-hart) | Empfohlen |
| Argon2id (OWASP-Min.) | Wenige Hundert Hashes/s | Ja | Ja | Empfohlen (bevorzugt) |
In der Praxis bedeutet das: Wer heute noch MD5, SHA-1 oder unsalted SHA-256 für Passwörter einsetzt, sollte umgehend auf bcrypt oder Argon2id migrieren, idealerweise über das in Abschnitt 7 beschriebene Rehash-on-Login-Muster. Zwischen bcrypt und Argon2id ist Argon2id die technisch modernere Wahl mit besserer Resistenz gegen spezialisierte Hardware, während bcrypt durch seine 25-jährige Praxiserfahrung und universelle Verfügbarkeit weiterhin eine solide, gut auditierte Alternative bleibt.
Mironsoft
Security-Audits, PHP-Härtung und Magento-Sicherheitsberatung
Passwort-Hashing professionell absichern?
Wir prüfen eure Authentifizierungslogik, migrieren Legacy-Hashes sicher auf Argon2id oder bcrypt und implementieren Rehash-on-Login, Rate-Limiting und weitere Absicherungen gegen Credential-Angriffe.
Security-Audit
Code-Review der Authentifizierung und Prüfung von Salt- und Cost-Faktor-Konfiguration
Migration
Legacy-Hashes schrittweise per Rehash-on-Login migrieren, ohne Zwangs-Reset
Hardening
Rate-Limiting, Account-Lockout und Monitoring gegen Credential-Stuffing
10. Zusammenfassung
Sicheres Passwort-Hashing mit bcrypt und Argon2 löst ein klares Problem: MD5 und SHA-1 sind für Checksummen gebaut, nicht für Passwörter, und ihre Geschwindigkeit macht sie nach einem Datenbank-Leak zur offenen Tür für Brute-Force und Rainbow-Tables. bcrypt bringt 25 Jahre Praxiserfahrung, universelle Verfügbarkeit und einen exponentiell einstellbaren Cost-Faktor mit. Argon2id ergänzt das um Memory-Hardness, die spezialisierte GPU- und ASIC-Angriffe wirtschaftlich unattraktiv macht, und ist seit RFC 9106 die empfohlene Standardwahl für neue Systeme.
PHP's password_hash() und password_verify() kapseln Salt-Generierung, Algorithmus und Cost-Faktor vollständig und nehmen Entwicklern die fehleranfällige manuelle Implementierung ab. password_needs_rehash() im Login-Flow sorgt dafür, dass Cost-Faktor-Erhöhungen und Algorithmus-Wechsel automatisch und graduell bei aktiven Nutzern ankommen, ohne einen erzwungenen Passwort-Reset für die gesamte Nutzerbasis. Wer diese Bausteine konsequent kombiniert und typische Fehler wie reversible Verschlüsselung, statische Salts oder unbedachte Passwort-Truncation vermeidet, reduziert das Risiko eines katastrophalen Credential-Leaks erheblich.
Passwort-Hashing: bcrypt, Argon2 und häufige Fehler - Das Wichtigste auf einen Blick
MD5/SHA-1 vermeiden
Fast und ohne eingebauten Salt, deshalb Milliarden Hashes/Sekunde per GPU knackbar. Niemals für Passwörter verwenden.
Salt & Cost-Faktor
Pro Nutzer zufälliger Salt macht Rainbow-Tables sinnlos. Cost-Faktor regelmäßig an schnellere Hardware anpassen.
bcrypt vs. Argon2id
bcrypt: bewährt, CPU-hart, 72-Byte-Limit. Argon2id: memory-hard, GPU/ASIC-resistent, seit RFC 9106 empfohlen.
Rehash-on-Login
password_needs_rehash() nach jedem erfolgreichen Login prüfen und Hashes graduell migrieren.