Vor gut dreißig Jahren war es ein Fortschritt, überhaupt irgendeine Art von kryptografischer Funktion in eine Programmiersprache einzubauen, die auf Bits und Ciphers basiert. Die UNIX-Funktion crypt() gehörte deshalb zu den frühen Stars in PHP 3 und 4, weil sie mit einer einzigen Zeile Code etwas tat, was bis dahin nur mit externen C-Bibliotheken möglich war: eine Zeichenkette in einen scheinbar unlesbaren Zeichensalat verwandeln.

Das klassische Beispiel – und das haben unzählige Lehrbücher bis heute unverändert abgedruckt – ist das Speichern eines Passworts:
$hash = crypt('passwort');
Doch die Zeiten haben sich dramatisch verändert. Rechenleistung ist milliardenfach gewachsen, dedizierte Passwort-Cracker (z. B. Hashcat) durchsuchen in Sekundenbruchteilen komplette Wörterbücher, und selbst Mittelklasse-Grafikkarten erreichen heute Billionen Hashes pro Sekunde. Was früher als „ausreichend sicher“ galt, ist für aktuelle Angreifer kaum mehr als ein feuchter Pappdeckel.
Genau hier liegt das Problem von crypt() – und die dringende Motivation für diesen Artikel. Wir werden
-
kurz erklären, was crypt() eigentlich macht und warum es historisch so populär war,
-
konkret zeigen, warum seine Verwendung inzwischen gefährlich ist,
-
die sichere, eingebaute Modern-Alternative password_hash() samt Ökosystem (password_verify(), password_needs_rehash(), password_algos()) vorstellen,
-
einen praxiserprobten Migrationspfad skizzieren, der alte crypt()-Hashes nahtlos in sichere Hashes überführt, und
-
häufige Missverständnisse aufräumen – vor allem die Verwechslung von Hashing und Verschlüsselung.
Dabei integrieren wir den klassischen Lehrbuchcode (siehe Ursprungs-Text) und zerlegen ihn Schritt für Schritt, um zu verdeutlichen, wo genau die Fallstricke liegen. Am Ende sollen Sie nicht nur wissen, warum crypt() nicht mehr zeitgemäß ist, sondern vor allem wie Sie Ihre Anwendung innerhalb weniger Stunden auf ein zukunftssicheres Fundament stellen.
1 Was ist crypt() und wofür wurde es früher verwendet?
crypt(string $password, string $salt = null): string ist eine Einweg-Hashfunktion. „Einweg“ bedeutet: Aus dem Rückgabewert (dem Hash) lässt sich das Originalpasswort mit vertretbarem Aufwand nicht zurückrechnen. Der klassische Zweck war also, Passwörter „gesichert“ abzulegen, ohne sie im Klartext zu speichern.
Der Ursprungs-Text demonstriert das sehr anschaulich:
$zeichenkette = 'passwort';
echo 'Ohne SALT: '.crypt($zeichenkette).PHP_EOL;
echo 'Mit SALT: '.crypt($zeichenkette, 'test').PHP_EOL;
-
Ohne Salt erzeugt crypt() automatisch ein zufälliges Salt. Jeder Funktionsaufruf liefert daher einen anderen Hash.
-
Mit Salt kommt reproduzierbare Konsistenz ins Spiel: solange dieselbe Passwort-Salt-Kombination eingegeben wird, ist der Hash identisch.
Die Mechanik dahinter adaptierte über die Jahre unterschiedliche UNIX-Algorithmen (DES, Extended DES, MD5, Blowfish, SHA-256, SHA-512 …). Welcher Algorithmus tatsächlich zur Verfügung steht, hängt dabei vom Betriebssystem und von der zur Laufzeit geladenen C-Bibliothek ab – ein erster Hinweis auf die Inkonsistenz, die wir später noch genauer beleuchten.
Historischer Nutzen
Ende der 1990er-Jahre war das alles trotzdem ein Segen. Legacy-Passwortdateien /etc/shadow setzten bereits auf DES- oder MD5-basierte crypt()-Hashes, und PHP-Entwickler konnten mit demselben Aufruf kompatible Hashes erzeugen. In Zeiten von 300-MHz-Pentium-II-CPUs war DES trotz seiner Schwächen realistisch kaum in Sekunden bruteforce-bar. Praktikabel, bewährt, schnell – gut genug.

Dass aus „gut genug“ inzwischen brandgefährlich geworden ist, liegt an vier Entwicklungen:
-
Mooresches Gesetz & Grafikprozessoren: rohe Hash-Power stieg millionenfach.
-
Öffentliche Rainbow-Tables für DES/MD5 wandelten offline-Angriffe in trivialen Nachschlage-Aufwand.
-
Sicherheitserkenntnisse: Algorithmen wie DES haben strukturelle Schwächen (z. B. effektive Länge von nur 56 Bit Schlüssel).
-
Komfortfunktionen in PHP ab Version 5.5 (2013): password_hash() & Co. machten sicheres Hashing kinderleicht.
2 Die Sicherheitsprobleme von crypt() im Detail
Trotz seiner langen Geschichte weist crypt() zahlreiche Schwächen auf, die in modernen Anwendungen erhebliche Sicherheitsrisiken darstellen. Die folgenden Abschnitte beleuchten diese im Detail.
2.1 Veraltete Algorithmen
Viele crypt()-Modi sind kryptografisch nicht länger ausreichend:
Modus | Maximale Schlüssellänge | Stand 2025 |
DES | 56 Bit | seit 1999 gebrochen (Distributed .net DES) |
MD5 | 128 Bit Hash, aber schnelles Design | 2004 erste Kollisionsangriffe; 2012 praktisch |
SHA-256/512 | 256/512 Bit | zwar kryptografisch robust, aber viel zu schnell für Passwort-Hashing |
Blowfish (BCrypt) | variabel | heute noch solide, aber crypt() bietet nur begrenzt Cost-Kontrolle |
Ein Kennwort, das 1998 in einem 13-Zeichen-MD5-Hash gespeichert wurde, ist 2025 oft in weniger als einer Sekunde vorhanden – entweder als Fund in öffentlichen „Have I Been Pwned“-Dumps oder nach kurzem GPU-Bruteforce.
Unter Linux mit glibc 2.38 stehen sieben Hash-Formate zur Verfügung, während Windows-PHP-Builds häufig nur Blowfish und MD5 kennen. Ein Hash, der auf System A erzeugt wurde, lässt sich auf System B eventuell nicht verifizieren.
Für Entwicklerteams mit Docker-Containern, CI/CD-Pipelines und mehreren Zielplattformen ist das ein Albtraum.
2.3 Fehleranfällige Salt-Handhabung
crypt() erwartet einen von außen mitgebrachten Salt (außer Sie lassen PHP automatisch einen generieren). Die Regeln dafür unterscheiden sich je nach Algorithmus – DES braucht exakt zwei ASCII-Zeichen, SHA-512 hingegen 66rounds=5000$ + ≥ 8 Zeichen Salt. Solche Details vergessen Menschen leicht. Ein zu kurzes Salt oder ein Copy-&-Paste-Fehler kann die gesamte Passwortdatenbank entwerten, weil identische Passwörter nun identische Hashes produzieren.
2.4 Keine zeitgemäße Cost-Funktion
Moderne Passwort-Hashalgorithmen verlangsamen jeden Hashvorgang absichtlich (Stichwort Key-Stretching). Dadurch wird Brute-Force rechnerisch teuer. crypt() bietet dies nur für Blowfish (via 2y$-Präfix) und auch dort ist die Konfiguration umständlich: Sie müssen den Logarithmus der Runden in den Salt-String einbetten. Fehler sind vorprogrammiert.
2.5 Nur die ersten acht Zeichen zählen – bei DES
Im DES-Modus ignoriert crypt() alles nach dem achten Zeichen. Das ist nicht offensichtlich und führt dazu, dass „LangPasswort!“ und „LangPasswort!MitZusatz“ denselben Hash liefern. Angreifer lieben solche verkappten Schwachstellen.
3 password_hash() – das sichere Standard-Werkzeug ab PHP 5.5
2013 führte PHP 5.5 eine neue High-Level-API ein:
string password_hash(
string $password,
string|int|null $algo = PASSWORD_DEFAULT,
array $options = []
): string
3.1 Warum password_hash() besser ist
Die folgende Tabelle zeigt zentrale Unterschiede zwischen crypt() und password_hash() – insbesondere im Hinblick auf Sicherheit, Wartbarkeit und Handhabung.
Feature | crypt() | password_hash() |
Sichere Algorithmen | abhängig vom Betriebssystem | BCrypt, Argon2i, Argon2id (alle bewährt & einstellbar) |
Salt-Generierung | manuell, fehleranfällig | automatisch, kryptografisch sicher |
Kostenparameter | nur Blowfish umständlich | einheitlich via cost bzw. memory_cost, time_cost |
Zukunftssicherheit | kein Auto-Upgrade | PASSWORD_DEFAULT wandert mit PHP-Version mit |
API-Einfachheit | 2 Parameter, viel Vorwissen nötig | 1 Funktion + 2 Hilfsfunktionen reichen |
Kurz: Sie müssen nichts über Salt-Längen, Präfixe oder DES-Fallstricke wissen. Sie schreiben:
$hash = password_hash($plainPassword, PASSWORD_DEFAULT);
und PHP erledigt den Rest absolut korrekt.
3.2 password_verify() – Passwörter prüfen
if (password_verify($loginEingabe, $hashAusDatenbank)) {
// Erfolg
}
Die Funktion erkennt automatisch, welcher Algorithmus im Hash kodiert ist (er steckt als Präfix darin) und nutzt den passenden Verifizierungsweg. Gleichzeitig schützt sie vor Timing-Angriffen – etwas, das viele Eigenbauten mit hash_equals() oder === falsch machen.
3.3 password_needs_rehash() – Hashes aktuell halten
Gestern haben Sie Ihre Anwendung auf Argon2id mit memory_cost = 6 560 kB aktualisiert, heute möchten Sie die Kosten verdoppeln? Kein Problem:
$options = ['memory_cost' => 131072, 'time_cost' => 4];
if (password_needs_rehash($hashAusDb, PASSWORD_ARGON2ID, $options)) {
$hashNeu = password_hash($plainPassword, PASSWORD_ARGON2ID, $options);
// in DB speichern
}
Beim nächsten Login aktualisieren Sie Hashes ganz nebenbei, ohne dass Benutzer etwas merken.
3.4 password_algos() – verfügbare Algorithmen abfragen
Ein kurzer Blick genügt:
print_r(password_algos());
// → Array ( [0] => 2y [1] => argon2i [2] => argon2id )
Damit können Installer-Skripte oder Diagnose-Tools prüfen, ob alle Zielserver dieselben Möglichkeiten bieten.
4 Migration von alten crypt()-Hashes
Bevor bestehende crypt()-Hashes durch moderne Alternativen ersetzt werden können, braucht es eine Migrationsstrategie, die Benutzer nicht stört und bestehende Logins nicht unterbricht.
4.1 Prinzip der Lazy Migration
-
Login des Benutzers: Passwort-Eingabe wird entgegengenommen.
-
password_verify() prüft das Passwort gegen den gespeicherten Hash. Ja, wirklich! Die Funktion erkennt auch klassische crypt()-Formate (alle außer sehr alten DES-Hashes ohne Präfix).
-
Wenn korrekt → sofort neues, sicheres Hash mit password_hash() bilden.
-
Hash in der Datenbank ersetzen, Session starten, fertig.
So wird jeder Benutzer automatisch migriert, sobald er sein Konto nutzt. Keine Massen-Passwort-Resets, keine Wartungsfenster.
4.2 Code-Beispiel
$userInput = $_POST['password'];
$oldHash = $row['password']; // kommt aus SELECT …
if (password_verify($userInput, $oldHash)) {
if (password_needs_rehash($oldHash, PASSWORD_DEFAULT)) {
$new = password_hash($userInput, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?');
$stmt->execute([$new, $row['id']]);
}
// normale Login-Routine
} else {
// Fehler
}
4.3 Was tun mit unleserlichen DES-Hashes?
Manche Uralt-Systeme nutzen zweistellige Salts und keine Präfixe. password_verify() erkennt sie nicht. Hier bleiben nur zwei Wege:
-
Forcierter Passwort-Reset: Benutzer klickt auf „Passwort vergessen“, bekommt Mail, setzt neues Passwort – jetzt mit modernen Hashes.
-
Offline-Rehash nach Klartext-Extraktion – nicht zu empfehlen, weil dazu sämtliche Passwörter im Klartext vorliegen müssen (riesiger Compliance-Bruch).
5 Hashing ≠ Verschlüsseln – häufige Verwechslungen
-
Hashing ist Einweg (irreversibel). Geeignet für Passwörter.
-
Verschlüsselung ist Zweiweg (reversibel). Geeignet für vertrauliche Daten wie Kreditkartennummern, die man lesen können muss.
PHP bietet dafür:
Aufgabe | Moderne API |
Symmetrische Verschlüsselung | sodium_crypto_secretbox_* (Libsodium) |
Asymmetrische Verschlüsselung | sodium_crypto_box_* / OpenSSL (openssl_*) |
Passwort-Hashing | password_hash(), password_verify() |
mcrypt ist seit PHP 7.2 entfernt, sein Einsatz gilt als höchst unsicher.
6 Best-Practice-Checkliste für sicheres Passwort-Handling in PHP (2025-Edition)
-
PHP ≥ 8.1 einsetzen – Argon2id ist standardmäßig verfügbar.
-
password_hash($pwd, PASSWORD_DEFAULT) (Argon2id) nutzen, als Fallback PASSWORD_BCRYPT.
-
Datenbank-Feldbreite mindestens VARCHAR(255) – ein Argon2id-Hash ist ~97 Zeichen lang.
-
Immer password_verify() statt manueller Vergleiche nutzen.
-
In Login-Routine password_needs_rehash() einbauen, um Kostenparameter aktuell zu halten.
-
Nach Möglichkeit Rate-Limiting und/oder captcha nach X Fehlversuchen implementieren.
-
Pepper (zusätzlicher geheimer Schlüssel auf Server) kann Brute-Force-Aufwand weiter erhöhen, sollte aber sorgsam verwaltet werden (z. B. in ENV- Variablen außerhalb des Repos).
-
2-Faktor-Authentifizierung ergänzt Passwort-Sicherheit, ersetzt sie aber nicht.
-
Offene Bibliotheken wie Symfony Security oder Laravel Sanctum verwenden, anstatt eigene Auth-Frameworks zu basteln.
-
Regelmäßige Audits: Hash-Kosten jedes Jahr prüfen; Moderne GPUs werden schneller.
7 Zusammenfassung & Handlungsaufforderung
-
crypt() war lange ein akzeptabler Kompromiss, ist 2025 aber faktisch unsicher.
-
Sein Hauptproblem ist nicht ein einzelner Bug, sondern eine Kombination aus zu schnellen Algorithmen, plattformspezifischem Verhalten und komplizierter Salt-Verwaltung.
-
PHP liefert seit über zehn Jahren eine einfachere, stärkere und zukunftssichere Lösung: password_hash() samt Ökosystem.
-
Die Umstellung erfordert wenige Code-Zeilen und kann per Lazy Migration ohne Benutzereingriff erfolgen.
-
Jeder Tag, den ein Produktivsystem noch crypt() nutzt, ist ein Tag, an dem Angreifer weniger Rechenzeit benötigen.
Handeln Sie jetzt: Suchen Sie in Ihrem Projekt nach crypt(, ersetzen Sie jeden Fund durch password_hash() / password_verify(), und implementieren Sie die oben gezeigte Migrationsroutine. So schützen Sie nicht nur Passwörter, sondern auch das Vertrauen Ihrer Nutzer – und schlafen selbst besser.
Vollständiges Beispielskript
<?php
declare(strict_types=1);
require 'db.php'; // PDO $pdo
function register(string $user, string $pwd): void
{
global $pdo;
$hash = password_hash($pwd, PASSWORD_DEFAULT);
$pdo->prepare('INSERT INTO users (name, password) VALUES (?,?)')
->execute([$user, $hash]);
}
function login(string $user, string $pwd): bool
{
global $pdo;
$stmt = $pdo->prepare('SELECT id, password FROM users WHERE name = ? LIMIT 1');
$stmt->execute([$user]);
$row = $stmt->fetch();
if (!$row || !password_verify($pwd, $row['password'])) {
return false;
}
// Hash ggf. aktualisieren
if (password_needs_rehash($row['password'], PASSWORD_DEFAULT)) {
$new = password_hash($pwd, PASSWORD_DEFAULT);
$pdo->prepare('UPDATE users SET password = ? WHERE id = ?')
->execute([$new, $row['id']]);
}
$_SESSION['uid'] = $row['id'];
return true;
}
Dieses kompakte Snippet zeigt alle Kernkonzepte in Aktion – registrieren, einloggen, rehashen. Dank password_*-Funktionen ist es sicher, portabel und wartungsarm.
Schlusswort
Sicherheitslücken entstehen selten durch spektakuläre Zero-Day-Exploits. Meist wurzeln sie in altem Code, den „schon immer alles funktioniert hat“. crypt() ist das Paradebeispiel: Er erfüllt bis heute seinen Vertrag – ein String geht rein, ein Hash kommt raus. Aber der Kontext hat sich geändert.
Jetzt ist es an Ihnen, die Lücke zwischen Historie und Gegenwart zu schließen. Mit password_hash() ist das so einfach wie nie zuvor. Machen Sie den Schritt – heute.