Kort gezegd: Veilige tokens genereert u in PHP met random_bytes(), bijvoorbeeld 32 bytes, die u met bin2hex() of Base64-URL in tekst omzet. Voor willekeurige getallen binnen een bereik is er random_int(), voor codes uit een vaste tekenvoorraad sinds PHP 8.3 Random\Randomizer::getBytesFromString(). Tokens slaat u op als hash, u beperkt hun geldigheid, gebruikt ze maar één keer en vergelijkt ze met hash_equals().
Een link die te gemakkelijk te raden is
"Wachtwoord vergeten?" Eén klik en het systeem verstuurt een link met een lange code. Wie de link kent, kan het wachtwoord instellen. Juist daarom moet deze code onvoorspelbaar zijn. Is dat niet zo, dan heeft een aanvaller noch de mailbox noch het oude wachtwoord nodig, alleen genoeg pogingen.
Bij controles van PHP-applicaties worden verrassend vaak tokens aangetroffen die zijn opgebouwd uit de tijd, het gebruikers-ID of een eenvoudige toevalsgenerator. Ze zien er lang en willekeurig uit, maar zijn dat niet. Hetzelfde geldt voor API-sleutels, uitnodigingslinks, deellinks voor documenten, bevestigingscodes en CSRF-tokens.
Het risico is concreet: een voorspelbare resetlink maakt de overname mogelijk van elk account waarvan het e-mailadres bekend is, zonder dat de eigenaar iets merkt, tot hij zelf niet meer kan aanmelden.
PHP biedt sinds versie 7.0 een eenvoudige en veilige oplossing. Ze moet alleen consequent worden gebruikt.
random_bytes in PHP: waar veilige willekeurige waarden vandaan komen
random_bytes() levert volgens de PHP-handleiding cryptografisch veilige willekeurige bytes, die ook geschikt zijn voor langlevende geheimen zoals sleutels. De functie gebruikt de bron voor willekeur van het besturingssysteem, onder Linux bijvoorbeeld getrandom(), onder Windows de CNG-interface. Vindt ze geen geschikte bron, dan gooit ze sinds PHP 8.2 een Random\RandomException.
Het overzicht toont welke functies voor geheimen deugen:
| Functie | Geschikt voor tokens? | Opmerking |
|---|---|---|
random_bytes() |
ja | veilige bytes, sinds PHP 7.0 |
random_int() |
ja | veilig getal binnen een bereik, sinds PHP 7.0 |
Random\Randomizer met secure-engine |
ja | Secure is volgens de handleiding de standaardengine |
rand(), mt_rand() |
nee | voorspelbaar, alleen voor niet-beveiligingsrelevante doeleinden |
uniqid() |
nee | tijdgebaseerd, bedoeld voor uniciteit |
md5(time()), sha1($id) |
nee | invoer te raden, de hash verandert daar niets aan |
str_shuffle(), array_rand() |
nee | gebruiken geen veilige generator |
Tokens genereren: lengte en codering
32 willekeurige bytes komen overeen met 256 bit en zijn voor vrijwel elk doel meer dan genoeg. Voor het transport in links of headers moet het token in tekst worden omgezet. Twee varianten zijn gangbaar:
<?php
declare(strict_types=1);
// Hex: 64 tekens, alleen 0-9 en a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 tekens, veilig in URL's zonder verdere codering
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Willekeurig getal binnen een bereik, bijvoorbeeld voor een keuze
$index = random_int(0, 9);
// Vanaf PHP 8.3: zescijferige cijfercode, bijv. voor een bevestiging per sms
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Korte cijfercodes zijn alleen verdedigbaar als het aantal pogingen streng is beperkt en de code na enkele minuten vervalt. Voor links die per e-mail worden verstuurd, neemt u altijd het lange token.
Voor API-sleutels die partners of eigen systemen blijvend gebruiken, hebben twee aanvullingen zich bewezen. Een kort, niet geheim voorvoegsel vóór het willekeurige deel, bijvoorbeeld voor omgeving of doel, vergemakkelijkt het herkennen in logboeken en bij vondsten in coderepositories. En de sleutel wordt maar één keer bij het aanmaken getoond; daarna kent het systeem alleen nog zijn hash. Raakt een sleutel kwijt, dan wordt hij vervangen, niet hersteld.
Voorzichtig met zelfgebouwde tokens die informatie bevatten, bijvoorbeeld gebruikers-ID en vervaltijd in Base64. Codering is geen beveiliging: iedereen kan zulke tokens lezen en wijzigen. Moet een token gegevens dragen, dan moet het met een geheime sleutel zijn ondertekend, bijvoorbeeld met hash_hmac('sha256', ...), en wordt de handtekening met hash_equals() gecontroleerd. Voor complexere gevallen verdienen geteste bibliotheken de voorkeur boven eigen constructies.
Tokens opslaan, controleren en laten vervallen
Een veilig gegenereerd token is maar het halve werk. Minstens zo belangrijk is hoe de applicatie ermee omgaat. Een beproefde werkwijze voor reset- en uitnodigingslinks:
- Alleen de hash opslaan: In de database staat
hash('sha256', $token), het token zelf gaat alleen via de link naar de gebruiker. - Geldigheid beperken: Vervaltijd mee opslaan en bij het inwisselen controleren.
- Eenmalig gebruiken: Na succesvol gebruik de vermelding verwijderen of als verbruikt markeren.
- Bij wijzigingen ongeldig maken: Na een wachtwoordwijziging alle openstaande reset-tokens van het account verwijderen.
- In constante tijd vergelijken: Waar een token rechtstreeks met een opgeslagen waarde wordt vergeleken,
hash_equals()gebruiken, met de opgeslagen waarde als eerste parameter.
<?php
// Genereren
$token = bin2hex(random_bytes(32));
$eintrag = [
'konto_id' => $kontoId,
'token_hash' => hash('sha256', $token),
'gueltig_bis'=> (new DateTimeImmutable('+30 minutes'))->format('Y-m-d H:i:s'),
];
// $token alleen in de link versturen, $eintrag opslaan
// Inwisselen
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// Vermelding via prepared statement zoeken op token_hash,
// verloop controleren, daarna de vermelding meteen verwijderen
Beperk bovendien het aantal resetverzoeken per account en periode, zodat de functie niet kan worden misbruikt voor het massaal verzenden van e-mails. Het antwoord op een resetverzoek mag bovendien niet verraden of het opgegeven adres überhaupt een account heeft.
Een snelle hash zoals SHA-256 volstaat hier, omdat het token zelf al lang en willekeurig is. Voor wachtwoorden die mensen kiezen, geldt dat niet; daar zijn trage algoritmen nodig, zoals Wachtwoorden in PHP veilig opslaan uitlegt.
Tokens horen niet in logbestanden. Controleer of uw webserver volledige URL's met queryparameters logt en of foutrapporten verzoekgegevens meesturen.
Na een incident: tokens en sleutels vernieuwen
Wie code op een server kon uitvoeren, had meestal ook toegang tot opgeslagen tokens, API-sleutels en sessiegegevens. Bestaat na PHP2Shell of een ander incident het vermoeden van een inbraak, vernieuw dan daarom niet alleen wachtwoorden, maar ook langlevende tokens en sleutels: API-toegangen, ondertekeningssleutels voor cookies of links, toegangen tot betaal- en verzenddiensten.
Sessies zijn een bijzonder geval: PHP genereert sessie-ID's zelf. Hoe u de instellingen daarvan beveiligt en sessies na een incident beëindigt, beschrijft de gids Sessies in PHP beveiligen. Voor formuliertokens tegen vervalste verzoeken, zie CSRF-bescherming in PHP-applicaties. Meer ingebouwde hulpmiddelen stelt Ingebouwde beveiligingsfuncties van PHP voor.
Hoe helpt PHP SOS concreet?
- Wij vinden in uw applicatie alle plekken waar tokens, codes en sleutels worden gegenereerd en vervangen onveilige bronnen.
- Wij controleren opslag, verloop en eenmalig gebruik van reset-, uitnodigings- en deellinks.
- Wij zorgen ervoor dat tokens niet in logboeken, foutrapporten of analysehulpmiddelen terechtkomen.
- Na een incident plannen wij samen met u de ordelijke vervanging van tokens en sleutels.
De controle van eigen PHP-software bieden wij aan als beveiligingsaudit en penetratietest.
Veelgestelde vragen
Waarom is mt_rand ongeschikt voor tokens?
mt_rand en rand zijn bedoeld voor statistiek en spelletjes, niet voor geheimen. Hun uitvoer is onder omstandigheden te voorspellen. Gebruik voor alles wat een aanvaller niet mag raden random_bytes of random_int.
Hoe lang moet een token zijn?
Voor links om wachtwoorden te herstellen, uitnodigingen of API-sleutels zijn 32 willekeurige bytes een solide keuze, als hextekst 64 tekens. 16 bytes, dus 128 bit, zijn een gangbare ondergrens.
Is uniqid een veilig token?
Nee. uniqid is gebaseerd op de actuele tijd en bedoeld voor uniciteit, niet voor geheimhouding. Ook combinaties zoals md5 van tijd of gebruikers-ID zijn voorspelbaar.
Moet ik reset-tokens in de database versleutelen?
Sla in plaats van het token een hash ervan op, bijvoorbeeld met SHA-256. Wie de database leest, kan daarmee geen geldige links bouwen. Omdat het token zelf al lang en willekeurig is, is geen traag algoritme nodig zoals bij wachtwoorden.
Wat gebeurt er als het systeem geen bron voor willekeur vindt?
random_bytes en random_int gooien sinds PHP 8.2 een Random\RandomException. Vang dit geval af en breek de handeling af, in plaats van uit te wijken naar een zwakkere bron.