Dabartinė padėtis: PHP2Shell. Du kritiniai PHP pažeidžiamumai (CVSS 9,2) leidžia vykdyti kenkėjišką kodą be prisijungimo. Pataisos nuo 2026-10-21.Ką dabar daryti →

Saugūs atsitiktiniai skaičiai ir žetonai PHP su random_bytes

Kaip kūrėjai PHP kuria žetonus slaptažodžio atstatymo nuorodoms, API raktams ir vienkartiniams kodams: kriptografiškai saugūs šaltiniai, pakankamas ilgis, saugojimas kaip maiša (hash) ir ribotas galiojimas.

Būsena 5 min. skaitymoSaugus programavimas

Trumpai: saugius žetonus PHP kuriate su random_bytes(), pavyzdžiui, 32 baitus, kuriuos su bin2hex() ar Base64-URL paverčiate tekstu. Atsitiktiniams skaičiams intervale yra random_int(), kodams iš fiksuoto simbolių rinkinio nuo PHP 8.3 Random\Randomizer::getBytesFromString(). Žetonus saugote kaip maišą (hash), ribojate jų galiojimą, naudojate tik vieną kartą ir lyginate su hash_equals().

Nuoroda, kurią per lengva atspėti

„Pamiršote slaptažodį?“ Vienas paspaudimas, ir sistema išsiunčia nuorodą su ilgu kodu. Kas nuorodą žino, gali nustatyti slaptažodį. Būtent todėl šis kodas turi būti nenuspėjamas. Jei ne, užpuolikui nereikia nei pašto dėžutės, nei senojo slaptažodžio, tik pakankamai bandymų.

Tikrinant PHP programas stebėtinai dažnai randami žetonai, sudaryti iš paros laiko, naudotojo ID ar paprasto atsitiktinių skaičių generatoriaus. Jie atrodo ilgi ir atsitiktiniai, bet tokie nėra. Tas pats galioja API raktams, kvietimo nuorodoms, dokumentų bendrinimo nuorodoms, patvirtinimo kodams ir CSRF žetonams.

Rizika apčiuopiama: nuspėjama atstatymo nuoroda leidžia perimti kiekvieną paskyrą, kurios el. pašto adresas žinomas, o savininkas nieko nepastebi, kol pats nebegali prisijungti.

PHP nuo 7.0 versijos siūlo paprastą ir saugų sprendimą. Reikia tik nuosekliai juo naudotis.

random_bytes PHP: iš kur ateina saugios atsitiktinės reikšmės

random_bytes() pagal PHP vadovą pateikia kriptografiškai saugius atsitiktinius baitus, tinkamus ir ilgalaikėms paslaptims, pavyzdžiui, raktams. Funkcija naudoja operacinės sistemos atsitiktinumo šaltinį, Linux sistemoje, pavyzdžiui, getrandom(), Windows sistemoje CNG sąsają. Jei tinkamo šaltinio neranda, nuo PHP 8.2 meta Random\RandomException.

Apžvalga parodo, kurios funkcijos tinka paslaptims:

Funkcija Ar tinka žetonams? Pastaba
random_bytes() taip saugūs baitai, nuo PHP 7.0
random_int() taip saugus skaičius intervale, nuo PHP 7.0
Random\Randomizer su Secure variklį taip pasak vadovo, Secure yra numatytasis variklis
rand(), mt_rand() ne nuspėjami, tik nesaugumui svarbiems tikslams
uniqid() ne pagrįsta laiku, skirta unikalumui
md5(time()), sha1($id) ne įvestis atspėjama, maiša (hash) to nekeičia
str_shuffle(), array_rand() ne nenaudoja saugaus generatoriaus

Žetonų kūrimas: ilgis ir kodavimas

32 atsitiktiniai baitai atitinka 256 bitus ir beveik bet kuriam tikslui yra daugiau nei pakankamai. Perduodant nuorodose ar antraštėse žetoną reikia paversti tekstu. Dažni du variantai:

<?php
declare(strict_types=1);

// Hex: 64 simboliai, tik 0-9 ir a-f
$tokenHex = bin2hex(random_bytes(32));

// Base64-URL: 43 simboliai, saugu URL be papildomo kodavimo
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');

// Atsitiktinis skaičius intervale, pavyzdžiui, pasirinkimui
$index = random_int(0, 9);

// Nuo PHP 8.3: šešiaženklis skaičių kodas, pvz., patvirtinimui per SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);

Trumpi skaičių kodai pateisinami tik tada, kai bandymų skaičius griežtai ribojamas, o kodas po kelių minučių nustoja galioti. Nuorodoms, siunčiamoms el. paštu, visada naudokite ilgą žetoną.

API raktams, kuriuos partneriai ar savos sistemos naudoja ilgai, pasiteisino du papildymai. Trumpas, ne slaptas prefiksas prieš atsitiktinę dalį, pavyzdžiui, aplinkai ar paskirčiai, palengvina atpažinimą žurnaluose ir radiniuose kodo saugyklose. O raktas rodomas tik vieną kartą kuriant; po to sistema žino tik jo maišą (hash). Jei raktas prarastas, jis pakeičiamas, o ne atkuriamas.

Atsargiai su pačių sukurtais žetonais, kuriuose yra informacijos, pavyzdžiui, naudotojo ID ir galiojimo laiko Base64 formatu. Kodavimas nėra apsauga: tokius žetonus gali perskaityti ir pakeisti kiekvienas. Jei žetonas turi nešti duomenis, jis turi būti pasirašytas slaptu raktu, pavyzdžiui, per hash_hmac('sha256', ...), o parašas tikrinamas su hash_equals(). Sudėtingesniais atvejais patikrintos bibliotekos verčiau už savo konstrukcijas.

Žetonų saugojimas, tikrinimas ir galiojimo pabaiga

Saugiai sukurtas žetonas yra tik pusė darbo. Bent jau tiek pat svarbu, kaip su juo elgiasi programa. Patikrinta eiga atstatymo ir kvietimo nuorodoms:

  1. Saugoti tik maišą (hash): duomenų bazėje yra hash('sha256', $token), pats žetonas tik nuoroda patenka naudotojui.
  2. Apriboti galiojimą: kartu išsaugoti pabaigos laiką ir tikrinti jį panaudojant.
  3. Naudoti vieną kartą: po sėkmingo panaudojimo įrašą ištrinti arba pažymėti kaip sunaudotą.
  4. Pakeitus panaikinti: pakeitus slaptažodį ištrinti visus atvirus paskyros atstatymo žetonus.
  5. Lyginti pastoviai: kur žetonas tiesiogiai lyginamas su išsaugota reikšme, naudoti hash_equals(), išsaugotą reikšmę dedant pirmu parametru.
<?php
// Sukurti
$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 siųsti tik nuorodoje, $eintrag išsaugoti

// Panaudoti
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// Įrašą ieškoti per token_hash su Prepared Statement,
// patikrinti galiojimą, po to įrašą nedelsiant ištrinti

Be to, apribokite atstatymo užklausų skaičių vienai paskyrai ir laikotarpiui, kad funkcija nebūtų išnaudota masiniam laiškų siuntimui. Atsakymas į atstatymo užklausą taip pat neturėtų išduoti, ar nurodytas adresas apskritai turi paskyrą.

Greitos maišos (hash) kaip SHA-256 čia pakanka, nes pats žetonas jau ilgas ir atsitiktinis. Žmonių pasirinktiems slaptažodžiams tai negalioja; ten reikia lėtų metodų, kaip aiškina Slaptažodžių saugojimas PHP saugiai.

Patarimas

Žetonams žurnalų failuose vietos nėra. Patikrinkite, ar jūsų žiniatinklio serveris žurnaluoja pilnus URL su užklausos parametrais ir ar klaidų ataskaitos kartu siunčia užklausos duomenis.

Po incidento: žetonų ir raktų atnaujinimas

Kas galėjo vykdyti kodą serveryje, paprastai turėjo prieigą ir prie išsaugotų žetonų, API raktų ir sesijos duomenų. Jei po PHP2Shell ar kito incidento kyla įsilaužimo įtarimas, turėtumėte atnaujinti ne tik slaptažodžius, bet ir ilgalaikius žetonus bei raktus: API prieigas, slapukų ar nuorodų parašo raktus, prieigas prie mokėjimo ir siuntų paslaugų.

Sesijos yra ypatingas atvejis: PHP sesijų ID kuria pats. Kaip apsaugoti jų nustatymus ir po incidento užbaigti sesijas, aprašo vadovas Sesijų PHP apsauga. Formų žetonams nuo suklastotų užklausų žr. CSRF apsauga PHP programose. Kitus įtaisytuosius įrankius pristato Įtaisytosios PHP saugumo funkcijos.

Kaip PHP SOS padeda konkrečiai?

  • Jūsų programoje randame visas vietas, kuriose kuriami žetonai, kodai ir raktai, ir pakeičiame nesaugius šaltinius.
  • Tikriname atstatymo, kvietimo ir bendrinimo nuorodų saugojimą, galiojimo pabaigą ir vienkartinį naudojimą.
  • Pasirūpiname, kad žetonai nepatektų į žurnalus, klaidų ataskaitas ar analizės įrankius.
  • Po incidento kartu su jumis planuojame tvarkingą žetonų ir raktų pakeitimą.

Savos PHP programinės įrangos patikrą siūlome kaip saugumo auditą ir įsiskverbimo testą.

Dažni klausimai

Kodėl mt_rand netinka žetonams?

mt_rand ir rand skirti statistikai ir žaidimams, o ne paslaptims. Jų išvestis tam tikromis aplinkybėmis galima nuspėti. Viskam, ko užpuolikas neturi atspėti, naudokite random_bytes arba random_int.

Kokio ilgio turėtų būti žetonas?

Slaptažodžio atstatymo nuorodoms, kvietimams ar API raktams 32 atsitiktiniai baitai yra solidus pasirinkimas, kaip hex tekstas 64 simboliai. 16 baitų, t. y. 128 bitai, yra paplitusi apatinė riba.

Ar uniqid yra saugus žetonas?

Ne. uniqid remiasi dabartiniu laiku ir skirtas unikalumui, o ne slaptumui. Ir deriniai kaip md5 iš laiko ar naudotojo ID yra nuspėjami.

Ar atstatymo žetonus duomenų bazėje turiu šifruoti?

Vietoj žetono išsaugokite jo maišą (hash), pavyzdžiui, su SHA-256. Kas skaito duomenų bazę, tuo negalės sudaryti galiojančių nuorodų. Kadangi pats žetonas jau ilgas ir atsitiktinis, lėto metodo kaip slaptažodžiams nereikia.

Kas atsitinka, jei sistema neranda atsitiktinumo šaltinio?

random_bytes ir random_int nuo PHP 8.2 meta Random\RandomException. Sučiupkite šį atvejį ir nutraukite procesą, užuot pereję prie silpnesnio šaltinio.

Šaltiniai

Skubus kontaktas

Kiekviena valanda svarbi. Parašykite mums dabar.

Trumpai aprašykite sistemą, PHP versiją ir pastebėjimus. Mes atsakysime, išsiaiškinsime padėtį ir dirbsime nuotoliu, kol incidentas bus užbaigtas ir dokumentuotas.

Skubi pagalba