Lyhyesti: Turvalliset tokenit luotte PHP:ssä funktiolla random_bytes(), esimerkiksi 32 tavua, jotka muunnatte tekstiksi funktiolla bin2hex() tai Base64-URL:lla. Satunnaisluvuille tietyltä väliltä on random_int(), kiinteästä merkistöstä koostuville koodeille PHP 8.3:sta alkaen Random\Randomizer::getBytesFromString(). Tokenit tallennatte tiivisteenä, rajaatte niiden voimassaolon, käytätte ne vain kerran ja vertaatte niitä funktiolla hash_equals().
Linkki, jonka arvaaminen on liian helppoa
”Unohtuiko salasana?” Yksi napsautus, ja järjestelmä lähettää linkin, jossa on pitkä koodi. Joka tuntee linkin, voi asettaa salasanan. Juuri siksi tämän koodin on oltava arvaamaton. Jos se ei ole, hyökkääjä ei tarvitse postilaatikkoa eikä vanhaa salasanaa, vain tarpeeksi yrityksiä.
PHP-sovellusten tarkistuksissa löytyy yllättävän usein tokeneita, jotka on rakennettu kellonajasta, käyttäjätunnuksesta tai yksinkertaisesta satunnaislukugeneraattorista. Ne näyttävät pitkiltä ja satunnaisilta, mutta eivät ole. Sama koskee API-avaimia, kutsulinkkejä, asiakirjojen jakolinkkejä, vahvistuskoodeja ja CSRF-tokeneita.
Riski on konkreettinen: ennustettava nollauslinkki mahdollistaa jokaisen tilin haltuunoton, jonka sähköpostiosoite tiedetään, ilman että omistaja huomaa mitään, ennen kuin hän ei itse pääse enää kirjautumaan.
PHP tarjoaa versiosta 7.0 alkaen yksinkertaisen ja turvallisen ratkaisun. Sitä tarvitsee vain käyttää johdonmukaisesti.
random_bytes PHP:ssä: mistä turvalliset satunnaisarvot tulevat
random_bytes() tuottaa PHP-käsikirjan mukaan kryptografisesti turvallisia satunnaistavuja, jotka sopivat myös pitkäikäisille salaisuuksille kuten avaimille. Funktio käyttää käyttöjärjestelmän satunnaislähdettä, Linuxissa esimerkiksi getrandom(), Windowsissa CNG-rajapintaa. Jos se ei löydä sopivaa lähdettä, se heittää PHP 8.2:sta alkaen poikkeuksen Random\RandomException.
Yleiskatsaus näyttää, mitkä funktiot kelpaavat salaisuuksille:
| Funktio | Sopii tokeneille? | Huomautus |
|---|---|---|
random_bytes() |
kyllä | turvallisia tavuja, PHP 7.0:sta alkaen |
random_int() |
kyllä | turvallinen luku tietyltä väliltä, PHP 7.0:sta alkaen |
Random\Randomizer Secure-moottorilla |
kyllä | Secure on käsikirjan mukaan oletusmoottori |
rand(), mt_rand() |
ei | ennustettavia, vain muuhun kuin tietoturvaan liittyvään käyttöön |
uniqid() |
ei | aikaperusteinen, tarkoitettu yksilöllisyyteen |
md5(time()), sha1($id) |
ei | syöte arvattavissa, tiiviste ei muuta tätä |
str_shuffle(), array_rand() |
ei | eivät käytä turvallista generaattoria |
Tokenien luonti: pituus ja koodaus
32 satunnaista tavua vastaa 256 bittiä ja on lähes mihin tahansa tarkoitukseen enemmän kuin tarpeeksi. Linkeissä tai otsikoissa siirtoa varten token on muunnettava tekstiksi. Kaksi vaihtoehtoa on yleistä:
<?php
declare(strict_types=1);
// Heksa: 64 merkkiä, vain 0-9 ja a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 merkkiä, turvallinen URL-osoitteissa ilman lisäkoodausta
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Satunnaisluku tietyltä väliltä, esimerkiksi valintaa varten
$index = random_int(0, 9);
// PHP 8.3:sta alkaen: kuusinumeroinen numerokoodi, esim. tekstiviestivahvistukseen
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Lyhyet numerokoodit ovat perusteltuja vain, jos yritysten määrä on tiukasti rajattu ja koodi vanhenee muutaman minuutin kuluttua. Sähköpostilla lähetettäviin linkkeihin käyttäkää aina pitkää tokenia.
API-avaimille, joita kumppanit tai omat järjestelmät käyttävät pysyvästi, kaksi lisäystä on osoittautunut hyväksi. Lyhyt, ei-salainen etuliite satunnaisosan edessä, esimerkiksi ympäristölle tai käyttötarkoitukselle, helpottaa tunnistamista lokeissa ja koodivarastojen löydöissä. Ja avain näytetään vain kerran luonnin yhteydessä; sen jälkeen järjestelmä tuntee vain sen tiivisteen. Jos avain katoaa, se korvataan, ei palauteta.
Varovaisuutta itse rakennettujen tokenien kanssa, jotka sisältävät tietoa, esimerkiksi käyttäjätunnuksen ja vanhenemisajan Base64-muodossa. Koodaus ei ole suojaus: jokainen voi lukea ja muuttaa tällaisia tokeneita. Jos tokenin on kuljetettava tietoa, se on allekirjoitettava salaisella avaimella, esimerkiksi hash_hmac('sha256', ...), ja allekirjoitus tarkistetaan funktiolla hash_equals(). Vaativampiin tapauksiin tarkistetut kirjastot ovat parempia kuin omat rakennelmat.
Tokenien tallennus, tarkistus ja vanhentaminen
Turvallisesti luotu token on vasta puolet työstä. Vähintään yhtä tärkeää on, miten sovellus käsittelee sitä. Hyväksi havaittu toimintatapa nollaus- ja kutsulinkeille:
- Tallenna vain tiiviste: Tietokannassa on
hash('sha256', $token), token itse menee käyttäjälle vain linkissä. - Rajaa voimassaolo: Tallentakaa vanhenemisaika ja tarkistakaa se lunastettaessa.
- Käytä kerran: Onnistuneen käytön jälkeen poistakaa merkintä tai merkitkää se käytetyksi.
- Mitätöi muutoksissa: Salasanan vaihdon jälkeen poistakaa tilin kaikki avoimet nollaustokenit.
- Vertaa vakioajassa: Missä token verrataan suoraan tallennettuun arvoon, käyttäkää funktiota
hash_equals()niin, että tallennettu arvo on ensimmäinen parametri.
<?php
// Luonti
$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'),
];
// Lähetä $token vain linkissä, tallenna $eintrag
// Lunastus
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// Hae merkintä valmistellulla lauseella token_hash-kentän perusteella,
// tarkista vanheneminen, poista sitten merkintä heti
Rajatkaa lisäksi nollauspyyntöjen määrä tiliä ja ajanjaksoa kohden, jottei toimintoa voi väärinkäyttää sähköpostien massalähetykseen. Nollauspyynnön vastauksen ei myöskään pitäisi paljastaa, onko annetulla osoitteella ylipäätään tiliä.
Nopea tiiviste kuten SHA-256 riittää tässä, koska token itse on jo pitkä ja satunnainen. Ihmisten valitsemille salasanoille tämä ei päde; niille tarvitaan hitaita menetelmiä, kuten Salasanojen turvallinen tallennus PHP:ssä selittää.
Tokenit eivät kuulu lokitiedostoihin. Tarkistakaa, kirjaako verkkopalvelimenne täydet URL-osoitteet kyselyparametreineen ja lähettävätkö virheraportit pyyntötietoja mukanaan.
Tapahtuman jälkeen: tokenien ja avainten uusiminen
Joka on voinut suorittaa koodia palvelimella, on yleensä päässyt käsiksi myös tallennettuihin tokeneihin, API-avaimiin ja istuntotietoihin. Jos PHP2Shellin tai muun tapahtuman jälkeen on epäilys tunkeutumisesta, uusikaa siksi salasanojen lisäksi myös pitkäikäiset tokenit ja avaimet: API-tunnukset, evästeiden tai linkkien allekirjoitusavaimet, pääsy maksu- ja toimituspalveluihin.
Sessiot ovat erikoistapaus: PHP luo sessiotunnukset itse. Miten niiden asetukset suojataan ja sessiot päätetään tapahtuman jälkeen, kuvaa opas Sessioiden suojaaminen PHP:ssä. Väärennettyjä pyyntöjä vastaan tarkoitetuista lomaketokeneista katso CSRF-suoja PHP-sovelluksissa. Muita sisäänrakennettuja työkaluja esittelee PHP:n sisäänrakennetut tietoturvatoiminnot.
Miten PHP SOS auttaa konkreettisesti?
- Löydämme sovelluksestanne kaikki kohdat, joissa tokeneita, koodeja ja avaimia luodaan, ja korvaamme turvattomat lähteet.
- Tarkistamme nollaus-, kutsu- ja jakolinkkien tallennuksen, vanhenemisen ja kertakäytön.
- Huolehdimme siitä, etteivät tokenit päädy lokeihin, virheraportteihin tai analyysityökaluihin.
- Tapahtuman jälkeen suunnittelemme kanssanne tokenien ja avainten hallitun vaihdon.
Oman PHP-ohjelmiston tarkistuksen tarjoamme tietoturva-auditointina ja tunkeutumistestinä.
Usein kysytyt kysymykset
Miksi mt_rand ei sovellu tokeneille?
mt_rand ja rand on tarkoitettu tilastoihin ja peleihin, ei salaisuuksille. Niiden tulosteet voidaan tietyissä oloissa ennustaa. Kaikkeen, mitä hyökkääjä ei saa arvata, käyttäkää random_bytesia tai random_intiä.
Kuinka pitkä tokenin pitäisi olla?
Salasanan nollauslinkeille, kutsuille tai API-avaimille 32 satunnaista tavua on vankka valinta, heksatekstinä 64 merkkiä. 16 tavua eli 128 bittiä on yleinen alaraja.
Onko uniqid turvallinen token?
Ei. uniqid perustuu nykyiseen aikaan ja on tarkoitettu yksilöllisyyteen, ei salassapitoon. Myös yhdistelmät kuten ajan tai käyttäjätunnuksen md5 ovat ennustettavissa.
Pitääkö nollaustokenit salata tietokannassa?
Tallentakaa tokenin sijaan sen tiiviste, esimerkiksi SHA-256:lla. Tietokannan lukija ei voi silloin rakentaa kelvollisia linkkejä. Koska token itse on jo pitkä ja satunnainen, hidasta menetelmää kuten salasanoille ei tarvita.
Mitä tapahtuu, jos järjestelmä ei löydä satunnaislähdettä?
random_bytes ja random_int heittävät PHP 8.2:sta alkaen poikkeuksen Random\RandomException. Käsitelkää tämä tapaus ja keskeyttäkää toimenpide sen sijaan, että turvautuisitte heikompaan lähteeseen.