Kort gezegd: Wachtwoorden slaat u in PHP op met password_hash() en controleert u met password_verify(). De standaard PASSWORD_DEFAULT gebruikt momenteel bcrypt, sinds PHP 8.4 met kostenfactor 12; waar PHP met Argon2 is gecompileerd, is PASSWORD_ARGON2ID een goed alternatief. Oude MD5- of SHA-1-hashes zet u om door ze meteen in password_hash te verpakken en bij de volgende login te vervangen door een directe hash.
Wat een databaselek met uw hashes te maken heeft
Na een inbraak in een webserver is de database een van de eerste doelwitten. Wie, zoals bij PHP2Shell mogelijk is, code op de server kan uitvoeren, leest ook de toegangsgegevens van de applicatie en daarmee de tabel met de gebruikersaccounts. Of de wachtwoorden daarin na uren of na jaren leesbaar worden, wordt niet beslist op de dag van de aanval, maar bij het programmeren.
Verrassend veel applicaties slaan wachtwoorden nog op met methoden die daar nooit voor bedoeld waren: MD5, SHA-1, een eenvoudige SHA-256 of zelfs omkeerbaar versleuteld. Deze methoden zijn snel en precies dat is het probleem. Aanvallers kunnen er heel veel kandidaten per seconde mee uitproberen.
PHP levert al lang een oplossing mee die langzaam rekent, voor elk wachtwoord automatisch een salt genereert en methode en parameters in de hash zelf opslaat. U hoeft ze alleen consequent te gebruiken.
password_hash in PHP: bcrypt of Argon2id?
password_hash() kent meerdere methoden. De keuze volgens de PHP-handleiding:
| Constante | Methode | Opmerking |
|---|---|---|
PASSWORD_DEFAULT |
momenteel bcrypt | Standaard, kan in toekomstige versies veranderen |
PASSWORD_BCRYPT |
bcrypt | Kostenfactor sinds PHP 8.4 standaard 12, daarvoor 10; alleen de eerste 72 bytes tellen |
PASSWORD_ARGON2I |
Argon2i | sinds PHP 7.2, alleen als PHP met Argon2-ondersteuning is gebouwd |
PASSWORD_ARGON2ID |
Argon2id | sinds PHP 7.3, zelfde voorwaarde |
OWASP adviseert Argon2id als eerste keuze, met minimaal 19 MiB geheugen, twee doorlopen en één thread, en bcrypt vooral voor bestaande systemen. Voor de meeste PHP-applicaties is het verschil in de praktijk kleiner dan het verschil tussen een van deze methoden en MD5. Belangrijker is dat u er überhaupt een gebruikt en de parameters regelmatig aanpast.
Of Argon2id beschikbaar is, laat een korte test zien:
<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());
Opslaan en controleren: een volledig voorbeeld
Het volgende voorbeeld toont registratie en aanmelding met automatische vernieuwing van verouderde hashes. De lengtecontrole beschermt tegen de 72-bytegrens van bcrypt:
<?php
declare(strict_types=1);
const PASSWORT_ALGO = PASSWORD_DEFAULT;
function passwortSpeichern(string $passwort): string
{
if (strlen($passwort) > 72) {
throw new InvalidArgumentException('Passwort zu lang für das gewählte Verfahren.');
}
return password_hash($passwort, PASSWORT_ALGO);
}
function passwortPruefen(string $eingabe, string $gespeicherterHash, callable $hashAktualisieren): bool
{
if (!password_verify($eingabe, $gespeicherterHash)) {
return false;
}
if (password_needs_rehash($gespeicherterHash, PASSWORT_ALGO)) {
$hashAktualisieren(password_hash($eingabe, PASSWORT_ALGO));
}
return true;
}
strlen() telt bytes, geen tekens. Dat is hier de bedoeling, want de bcrypt-grens heeft betrekking op bytes. De kolom in de database moet volgens de handleiding meer dan 60 tekens kunnen bevatten, 255 is een goede keuze. Stel nooit een eigen salt in: de optie is verouderd en wordt sinds PHP 8.0 genegeerd.
Met de overstap naar PHP 8.4 stijgt de standaardkostenfactor voor bcrypt van 10 naar 12. Bestaande hashes worden dankzij password_needs_rehash() bij de volgende login automatisch verhoogd. Op zwaar belaste systemen loont vóór de overstap een korte belastingstest van de aanmelding.
Meet de kostenfactor op uw productieserver. Een enkele controle moet merkbaar rekentijd kosten, maar ruim onder een seconde blijven, zodat aanmeldingen bij belasting geen knelpunt worden.
Oude MD5- en SHA-1-hashes vervangen
De eigenlijke uitdaging is meestal het bestaande bestand. De wachtwoorden zelf kent u niet, dus u kunt ze niet zomaar opnieuw hashen. Wachten tot elke gebruiker zich weer aanmeldt, laat inactieve accounts onbeschermd. OWASP beschrijft daarom een aanpak in twee fasen:
- Meteen verpakken: Elke bestaande MD5- of SHA-1-waarde met een script omhullen met
password_hash()en met een markering opslaan, bijvoorbeeld in een kolomhash_typ. - Bij de login controleren: Voor gemarkeerde accounts de invoer eerst met de oude methode omrekenen en daarna met
password_verify()controleren tegen de omhulde waarde. - Direct vervangen: Na succesvolle aanmelding een normale hash uit de leesbare tekst genereren en de markering verwijderen.
- Restant opschonen: Accounts die na een vastgestelde termijn nog omhulde hashes hebben, op wachtwoordreset zetten.
<?php
// Fase 1 (eenmalig via script): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Fase 2 en 3 bij de login:
if ($konto['hash_typ'] === 'md5-umhuellt') {
$ok = password_verify(md5($eingabe), $konto['passwort_hash']);
if ($ok) {
$speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
}
}
Omhulde hashes zijn beter dan kale MD5-waarden, maar zwakker dan directe hashes. Ze zijn een overgangsoplossing, geen doel. Na een incident waarbij de database getroffen kan zijn, vervangt deze omschakeling niet het verzoek aan gebruikers om hun wachtwoord te wijzigen. Welke sporen op zo'n incident wijzen, beschrijft Ben ik al gecompromitteerd?.
Pepper, versleuteling en andere dwaalwegen
Sommige ideeën klinken als meer veiligheid, maar leveren weinig op of schaden:
- Eigen hashconstructies zoals meervoudige SHA-256 met zelf gegenereerde salt zijn sneller dan bcrypt en daarom zwakker.
- Versleutelde wachtwoorden zijn omkeerbaar. Staat de sleutel op dezelfde server, dan beschermt de versleuteling na een inbraak nauwelijks.
- Een pepper, dus een geheime extra waarde voor alle wachtwoorden, kan een extra laag zijn als hij gescheiden van de database wordt bewaard. Hij vervangt echter geen goede hashmethode en bemoeilijkt latere wijzigingen.
- Vooraf hashen vóór bcrypt om de 72-bytegrens te omzeilen, brengt volgens OWASP eigen risico's mee. Een duidelijke lengtegrens of Argon2id is eenvoudiger.
Ook bij de regels voor nieuwe wachtwoorden loont een blik op de stand van de techniek: lengte beschermt meer dan ingewikkelde voorschriften over speciale tekens, en zeer gangbare wachtwoorden of wachtwoorden die al in datalekken zijn opgedoken, moeten worden geweigerd. Verplichte wijzigingen met vaste tussenpozen zonder aanleiding leiden volgens de ervaring eerder tot voorspelbare patronen dan tot sterkere wachtwoorden.
Voor aanmeldsystemen staan wachtwoorden bovendien nooit alleen: tweestapsverificatie voor beheerders en een rem op massale aanmeldpogingen vullen de opslag aan. Beide behandelt de gids Beheerlogins beschermen tegen brute force. Voor resetlinks en eenmalige codes gelden andere regels, zie Veilige willekeurige getallen en tokens in PHP. Een overzicht van verdere functies geeft Ingebouwde beveiligingsfuncties van PHP.
Hoe helpt PHP SOS concreet?
- Wij controleren hoe uw applicatie en uw plug-ins wachtwoorden opslaan en vinden verouderde methoden ook in nevenfuncties.
- Wij plannen de omschakeling van het bestaande bestand met omhulling, loginmigratie en termijn voor restaccounts.
- Wij stemmen kostenfactor of Argon2-parameters af op uw servers.
- Na een incident ondersteunen wij bij de beslissing over wachtwoordresets en de technische uitvoering daarvan.
Een controle van eigen PHP-software bieden wij aan als beveiligingsaudit en penetratietest.
Veelgestelde vragen
Welk algoritme gebruikt password_hash met PASSWORD_DEFAULT?
Momenteel bcrypt. De PHP-handleiding wijst erop dat de standaard kan veranderen wanneer sterkere methoden erbij komen. Daarom moet de databasekolom meer dan 60 tekens kunnen bevatten; de handleiding noemt 255 als goede keuze.
Moet ik wachtwoorden versleutelen in plaats van hashen?
Nee. Versleutelde wachtwoorden zijn met de sleutel weer te lezen en die sleutel staat meestal op dezelfde server. Een wachtwoordhash is een eenrichtingsstraat: de applicatie kan alleen controleren of een invoer klopt.
Hoe hoog moet de kostenfactor bij bcrypt zijn?
Sinds PHP 8.4 is de standaard 12, daarvoor 10. OWASP noemt 10 als ondergrens en adviseert zo hoog te gaan als uw servers aankunnen. Meet op de eigen hardware hoe lang een controle duurt.
Wat betekent de 72-bytegrens bij bcrypt?
bcrypt houdt alleen rekening met de eerste 72 bytes van een wachtwoord, alles daarna wordt afgekapt. Bij tekens met accenten of umlauten telt één teken vaak twee bytes. Begrens de lengte daarom uitdrukkelijk of gebruik Argon2id als uw PHP ermee is gecompileerd.
Moeten gebruikers na een inbraak hun wachtwoord wijzigen?
Als niet kan worden uitgesloten dat de database is weggelekt, moet u dat laten doen. Goede hashes vertragen het kraken aanzienlijk, maar zwakke wachtwoorden blijven kwetsbaar. Of er daarnaast een meldplicht geldt, stemt u af met uw functionaris voor gegevensbescherming.