Stručne: Bezpečné tokeny vytvoríte v PHP pomocou random_bytes(), napríklad 32 bajtov, ktoré pomocou bin2hex() alebo Base64-URL prevediete na text. Pre náhodné čísla v rozsahu slúži random_int(), pre kódy z pevnej zásoby znakov od PHP 8.3 Random\Randomizer::getBytesFromString(). Tokeny ukladajte ako hash, obmedzte ich platnosť, používajte ich len raz a porovnávajte ich pomocou hash_equals().
Odkaz, ktorý sa dá príliš ľahko uhádnuť
„Zabudli ste heslo?“ Jedno kliknutie a systém odošle odkaz s dlhým kódom. Kto odkaz pozná, môže nastaviť heslo. Práve preto musí byť tento kód nepredvídateľný. Ak nie je, útočník nepotrebuje ani schránku, ani staré heslo, len dosť pokusov.
Pri kontrolách aplikácií PHP sa prekvapivo často nájdu tokeny zložené z času, ID používateľa alebo jednoduchého generátora náhodných čísel. Vyzerajú dlho a náhodne, no nie sú. To isté platí pre kľúče API, pozývacie odkazy, odkazy na zdieľanie dokumentov, potvrdzovacie kódy a tokeny CSRF.
Riziko je hmatateľné: predvídateľný odkaz na reset umožňuje prevziať každý účet, ktorého e-mailová adresa je známa, bez toho, aby si majiteľ niečo všimol, kým sa sám neprestane vedieť prihlásiť.
PHP ponúka od verzie 7.0 jednoduché a bezpečné riešenie. Stačí ho dôsledne používať.
random_bytes v PHP: odkiaľ pochádzajú bezpečné náhodné hodnoty
random_bytes() poskytuje podľa príručky PHP kryptograficky bezpečné náhodné bajty, ktoré sa hodia aj pre dlhodobé tajomstvá ako kľúče. Funkcia používa zdroj náhodnosti operačného systému, pod Linuxom napríklad getrandom(), pod Windows rozhranie CNG. Ak nenájde vhodný zdroj, vyhodí od PHP 8.2 výnimku Random\RandomException.
Prehľad ukazuje, ktoré funkcie sa pre tajomstvá hodia:
| Funkcia | Vhodná pre tokeny? | Poznámka |
|---|---|---|
random_bytes() |
áno | bezpečné bajty, od PHP 7.0 |
random_int() |
áno | bezpečné číslo v rozsahu, od PHP 7.0 |
Random\Randomizer so Secure Engine |
áno | Secure je podľa príručky štandardný engine |
rand(), mt_rand() |
nie | predvídateľné, len na účely nedôležité pre bezpečnosť |
uniqid() |
nie | založené na čase, určené na jedinečnosť |
md5(time()), sha1($id) |
nie | vstup sa dá uhádnuť, hash na tom nič nemení |
str_shuffle(), array_rand() |
nie | nepoužívajú bezpečný generátor |
Vytváranie tokenov: dĺžka a kódovanie
32 náhodných bajtov zodpovedá 256 bitom a pre takmer každý účel je to viac než dosť. Na prenos v odkazoch alebo hlavičkách sa musí token previesť na text. Obvyklé sú dva varianty:
<?php
declare(strict_types=1);
// Hex: 64 znakov, len 0-9 a a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 znakov, bezpečné v URL bez ďalšieho kódovania
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Náhodné číslo v rozsahu, napríklad pre výber
$index = random_int(0, 9);
// Od PHP 8.3: šesťmiestny číselný kód, napr. na potvrdenie cez SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Krátke číselné kódy sú obhájiteľné len vtedy, keď je počet pokusov prísne obmedzený a kód po niekoľkých minútach zaniká. Pre odkazy posielané e-mailom vždy použite dlhý token.
Pre kľúče API, ktoré partneri alebo vlastné systémy používajú trvalo, sa osvedčili dva doplnky. Krátka, netajná predpona pred náhodnou časťou, napríklad pre prostredie alebo účel, uľahčuje rozpoznanie v protokoloch a pri nálezoch v repozitároch kódu. A kľúč sa zobrazí len raz pri vytvorení; potom systém pozná už len jeho hash. Ak sa kľúč stratí, nahradí sa, neobnovuje sa.
Pozor pri vlastnoručne zostavených tokenoch, ktoré obsahujú informácie, napríklad ID používateľa a čas platnosti v Base64. Kódovanie nie je zabezpečenie: každý môže takéto tokeny čítať a meniť. Ak má token niesť údaje, musí byť podpísaný tajným kľúčom, napríklad pomocou hash_hmac('sha256', ...), a podpis sa kontroluje pomocou hash_equals(). Pre náročnejšie prípady sú preverené knižnice vhodnejšie než vlastné konštrukcie.
Ukladanie, overovanie a zánik tokenov
Bezpečne vytvorený token je len polovica úspechu. Prinajmenšom rovnako dôležité je, ako s ním aplikácia zaobchádza. Osvedčený postup pre odkazy na reset a pozvánky:
- Ukladať len hash: v databáze je
hash('sha256', $token), samotný token ide používateľovi iba cez odkaz. - Obmedziť platnosť: uložiť čas zániku a skontrolovať ho pri uplatnení.
- Použiť raz: po úspešnom použití záznam zmazať alebo označiť ako spotrebovaný.
- Pri zmenách zneplatniť: po zmene hesla zmazať všetky otvorené tokeny na reset daného účtu.
- Porovnávať v konštantnom čase: kde sa token porovnáva priamo s uloženou hodnotou, použiť
hash_equals()s uloženou hodnotou ako prvým parametrom.
<?php
// Vytvorenie
$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 poslať len v odkaze, $eintrag uložiť
// Uplatnenie
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// záznam vyhľadať cez token_hash pomocou Prepared Statement,
// skontrolovať platnosť, potom záznam okamžite zmazať
Obmedzte navyše počet požiadaviek na reset na účet a časové obdobie, aby sa funkcia nedala zneužiť na hromadné odosielanie e-mailov. Odpoveď na požiadavku na reset by navyše nemala prezradiť, či zadaná adresa vôbec má účet.
Rýchly hash ako SHA-256 tu stačí, pretože samotný token je už dlhý a náhodný. Pre heslá, ktoré volia ľudia, to neplatí; tam sú potrebné pomalé postupy, ako vysvetľuje Bezpečné ukladanie hesiel v PHP.
Tokeny nepatria do súborov protokolov. Skontrolujte, či váš webserver nezaznamenáva úplné URL s parametrami query a či chybové správy neposielajú údaje požiadavky.
Po incidente: obnoviť tokeny a kľúče
Kto mohol spúšťať kód na serveri, mal spravidla prístup aj k uloženým tokenom, kľúčom API a údajom relácií. Ak po PHP2Shell alebo inom incidente vzniká podozrenie na vlámanie, mali by ste preto obnoviť nielen heslá, ale aj dlhodobé tokeny a kľúče: prístupy API, podpisové kľúče pre cookies alebo odkazy, prístupy k platobným a prepravným službám.
Relácie sú zvláštny prípad: ID relácií vytvára PHP samo. Ako zabezpečiť ich nastavenia a ukončiť relácie po incidente, opisuje sprievodca Zabezpečenie relácií v PHP. K tokenom vo formulároch proti sfalšovaným požiadavkám pozri Ochrana pred CSRF v aplikáciách PHP. Ďalšie zabudované nástroje predstavuje Zabudované bezpečnostné funkcie PHP.
Ako konkrétne pomáha PHP SOS?
- Nájdeme vo vašej aplikácii všetky miesta, kde sa vytvárajú tokeny, kódy a kľúče, a nahradíme nebezpečné zdroje.
- Preveríme ukladanie, zánik a jednorazové použitie odkazov na reset, pozvánok a odkazov na zdieľanie.
- Postaráme sa o to, aby tokeny neskončili v protokoloch, chybových správach ani analytických nástrojoch.
- Po incidente s vami naplánujeme usporiadanú výmenu tokenov a kľúčov.
Kontrolu vlastného softvéru PHP ponúkame ako bezpečnostný audit a penetračný test.
Časté otázky
Prečo je mt_rand nevhodný pre tokeny?
mt_rand a rand sú určené pre štatistiku a hry, nie pre tajomstvá. Ich výstupy sa za určitých okolností dajú predvídať. Pre všetko, čo útočník nesmie uhádnuť, použite random_bytes alebo random_int.
Aký dlhý by mal byť token?
Pre odkazy na obnovenie hesla, pozvánky alebo kľúče API sú solídnou voľbou 32 náhodných bajtov, ako text hex 64 znakov. 16 bajtov, teda 128 bitov, je rozšírená spodná hranica.
Je uniqid bezpečný token?
Nie. uniqid vychádza z aktuálneho času a je určený na jedinečnosť, nie na utajenie. Aj kombinácie ako md5 z času alebo ID používateľa sú predvídateľné.
Musím tokeny na reset v databáze šifrovať?
Namiesto tokenu ukladajte jeho hash, napríklad pomocou SHA-256. Kto číta databázu, nemôže z toho zostaviť platné odkazy. Keďže samotný token je už dlhý a náhodný, nepotrebuje pomalý postup ako pri heslách.
Čo sa stane, ak systém nenájde zdroj náhodnosti?
random_bytes a random_int vyhadzujú od PHP 8.2 výnimku Random\RandomException. Tento prípad zachyťte a proces prerušte, namiesto prechodu na slabší zdroj.