Pe scurt: Generați token-uri sigure în PHP cu random_bytes(), de exemplu 32 de octeți, pe care îi convertiți în text cu bin2hex() sau Base64-URL. Pentru numere aleatorii într-un interval există random_int(), iar pentru coduri dintr-un set fix de caractere, începând cu PHP 8.3, Random\Randomizer::getBytesFromString(). Token-urile le stocați ca hash, le limitați valabilitatea, le folosiți o singură dată și le comparați cu hash_equals().
Un link prea ușor de ghicit
„Ați uitat parola?” Un clic, iar sistemul trimite un link cu un cod lung. Cine cunoaște linkul poate seta parola. Tocmai de aceea acest cod trebuie să fie imprevizibil. Dacă nu este, un atacator nu are nevoie nici de căsuța poștală, nici de vechea parolă, ci doar de suficiente încercări.
La verificările aplicațiilor PHP se găsesc surprinzător de des token-uri construite din ora zilei, din ID-ul utilizatorului sau dintr-un generator simplu de numere aleatorii. Par lungi și aleatorii, dar nu sunt. Același lucru este valabil pentru chei API, linkuri de invitație, linkuri de partajare a documentelor, coduri de confirmare și token-uri CSRF.
Riscul este concret: un link de resetare previzibil permite preluarea oricărui cont a cărui adresă de e-mail este cunoscută, fără ca titularul să observe ceva, până când nu se mai poate autentifica el însuși.
PHP oferă începând cu versiunea 7.0 o soluție simplă și sigură. Trebuie doar folosită consecvent.
random_bytes în PHP: de unde provin valorile aleatorii sigure
random_bytes() furnizează, potrivit manualului PHP, octeți aleatorii siguri criptografic, potriviți și pentru secrete de lungă durată, precum cheile. Funcția folosește sursa de aleatoriu a sistemului de operare, sub Linux de exemplu getrandom(), sub Windows interfața CNG. Dacă nu găsește o sursă potrivită, aruncă, începând cu PHP 8.2, o Random\RandomException.
Prezentarea arată ce funcții sunt bune pentru secrete:
| Funcție | Potrivită pentru token-uri? | Observație |
|---|---|---|
random_bytes() |
da | Octeți siguri, din PHP 7.0 |
random_int() |
da | Număr sigur într-un interval, din PHP 7.0 |
Random\Randomizer cu motor Secure |
da | Secure este, potrivit manualului, motorul implicit |
rand(), mt_rand() |
nu | Previzibile, doar pentru scopuri nerelevante pentru securitate |
uniqid() |
nu | Bazat pe timp, conceput pentru unicitate |
md5(time()), sha1($id) |
nu | Intrare ghicibilă, hash-ul nu schimbă nimic |
str_shuffle(), array_rand() |
nu | Nu folosesc un generator sigur |
Generarea token-urilor: lungime și codare
32 de octeți aleatorii corespund la 256 de biți și sunt mai mult decât suficienți pentru aproape orice scop. Pentru transmiterea în linkuri sau antete, tokenul trebuie convertit în text. Două variante sunt uzuale:
<?php
declare(strict_types=1);
// Hex: 64 de caractere, doar 0-9 și a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 de caractere, sigur în URL-uri fără codare suplimentară
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Număr aleatoriu într-un interval, de exemplu pentru o selecție
$index = random_int(0, 9);
// Din PHP 8.3: cod numeric de șase cifre, de ex. pentru o confirmare prin SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Codurile numerice scurte sunt acceptabile doar dacă numărul de încercări este strict limitat, iar codul expiră după câteva minute. Pentru linkurile trimise prin e-mail, folosiți întotdeauna tokenul lung.
Pentru cheile API folosite durabil de parteneri sau de propriile sisteme s-au dovedit utile două completări. Un prefix scurt, nesecret, înaintea părții aleatorii, de exemplu pentru mediu sau scop, ușurează recunoașterea în jurnale și la descoperirea în depozite de cod. Iar cheia este afișată o singură dată, la creare; după aceea sistemul cunoaște doar hash-ul ei. Dacă o cheie se pierde, ea este înlocuită, nu restaurată.
Atenție la token-urile construite singuri care conțin informații, de exemplu ID-ul utilizatorului și timpul de expirare în Base64. Codarea nu este protecție: oricine poate citi și modifica astfel de token-uri. Dacă un token trebuie să poarte date, el trebuie semnat cu o cheie secretă, de exemplu prin hash_hmac('sha256', ...), iar semnătura se verifică cu hash_equals(). Pentru cazuri mai complexe, bibliotecile verificate sunt de preferat construcțiilor proprii.
Stocarea, verificarea și expirarea token-urilor
Un token generat sigur este doar jumătate din treabă. Cel puțin la fel de important este cum se comportă aplicația cu el. O procedură dovedită pentru linkuri de resetare și invitație:
- Stocați doar hash-ul: în baza de date apare
hash('sha256', $token), iar tokenul însuși ajunge la utilizator doar prin link. - Limitați valabilitatea: stocați și timpul de expirare și verificați-l la valorificare.
- Folosiți o singură dată: după utilizarea reușită, ștergeți înregistrarea sau marcați-o ca epuizată.
- Invalidați la modificări: după o schimbare de parolă, ștergeți toate token-urile de resetare deschise ale contului.
- Comparați în timp constant: acolo unde un token este comparat direct cu o valoare stocată, folosiți
hash_equals(), cu valoarea stocată ca prim parametru.
<?php
// Generare
$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'),
];
// Trimiteți $token doar în link, stocați $eintrag
// Valorificare
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// Căutați înregistrarea prin prepared statement după token_hash,
// verificați expirarea, apoi ștergeți imediat înregistrarea
Limitați în plus numărul de cereri de resetare pe cont și pe perioadă, pentru ca funcția să nu poată fi folosită abuziv la trimiterea în masă de e-mailuri. Răspunsul la o cerere de resetare nu ar trebui, de asemenea, să trădeze dacă adresa indicată are sau nu un cont.
Un hash rapid precum SHA-256 este aici suficient, deoarece tokenul însuși este deja lung și aleatoriu. Pentru parolele alese de oameni, acest lucru nu este valabil; acolo sunt necesare procedee lente, așa cum explică Stocarea sigură a parolelor în PHP.
Token-urile nu aparțin în fișierele jurnal. Verificați dacă serverul dumneavoastră web înregistrează URL-uri complete cu parametri de interogare și dacă rapoartele de erori trimit odată cu ele datele cererii.
După un incident: reînnoirea token-urilor și a cheilor
Cine a putut executa cod pe un server a avut, de regulă, acces și la token-uri stocate, chei API și date de sesiune. Dacă după PHP2Shell sau după un alt incident există suspiciunea unei intruziuni, ar trebui de aceea să reînnoiți nu doar parolele, ci și token-urile și cheile de lungă durată: accese API, chei de semnare pentru cookie-uri sau linkuri, accese la servicii de plată și expediere.
Sesiunile sunt un caz special: PHP generează singur ID-urile de sesiune. Cum securizați setările lor și cum încheiați sesiunile după un incident descrie ghidul Securizarea sesiunilor în PHP. Pentru token-urile de formular împotriva cererilor falsificate, vedeți Protecția CSRF în aplicațiile PHP. Alte instrumente integrate prezintă Funcțiile de securitate integrate ale PHP.
Cum ajută PHP SOS concret?
- Găsim în aplicația dumneavoastră toate locurile în care sunt generate token-uri, coduri și chei și înlocuim sursele nesigure.
- Verificăm stocarea, expirarea și utilizarea unică a linkurilor de resetare, invitație și partajare.
- Ne asigurăm că token-urile nu ajung în jurnale, rapoarte de erori sau instrumente de analiză.
- După un incident planificăm împreună cu dumneavoastră înlocuirea ordonată a token-urilor și cheilor.
Verificarea software-ului PHP propriu o oferim ca audit de securitate și test de penetrare.
Întrebări frecvente
De ce este mt_rand nepotrivit pentru token-uri?
mt_rand și rand sunt concepute pentru statistică și jocuri, nu pentru secrete. Ieșirile lor pot fi uneori prezise. Pentru tot ce un atacator nu are voie să ghicească, folosiți random_bytes sau random_int.
Cât de lung ar trebui să fie un token?
Pentru linkuri de resetare a parolelor, invitații sau chei API, 32 de octeți aleatorii sunt o alegere solidă, ca text hex 64 de caractere. 16 octeți, adică 128 de biți, sunt o limită inferioară răspândită.
Este uniqid un token sigur?
Nu. uniqid se bazează pe ora curentă și este conceput pentru unicitate, nu pentru confidențialitate. Și combinații precum md5 al timpului sau al ID-ului de utilizator sunt previzibile.
Trebuie să criptez token-urile de resetare în baza de date?
Stocați în loc de token un hash al lui, de exemplu cu SHA-256. Cine citește baza de date nu poate construi astfel linkuri valide. Deoarece tokenul însuși este deja lung și aleatoriu, nu are nevoie de un procedeu lent, ca la parole.
Ce se întâmplă dacă sistemul nu găsește o sursă de aleatoriu?
random_bytes și random_int aruncă, începând cu PHP 8.2, o Random\RandomException. Interceptați acest caz și întrerupeți operația, în loc să treceți la o sursă mai slabă.