Minulla on kulunut pidemmän aikaa siitä, kun olen viimeksi kirjautumisjärjestelmiä kirjoitellut ja ennenkin lähinnä pohjautuen antiikkisen phpBB2:n kekseihin ja GET-muuttujiin perustuvalle järjestelmälle, joten ajattelin tässä nopeasti varmistella pari asiaa/oletusta. Erityisesti tietoturvan kannalta.
Jos olen ymmärtänyt oikein, $_SESSION luodaan X minuutiksi PHP:n asetuksista riippuen ja sen kestoon ei kauheasti voi vaikuttaa mm. palvelimien ajamien sessioita poistavien cronien takia. $_SESSION kuitenkin ilmeisesti jatkaa olemassaoloaan aina vain pidemmälle jos käyttäjä pysyy säännöllisesti sivustolla kirjautuneena. Täten pikapyrähdysvierailuja varten minun ei tarvitse luoda keksiä vaan voin hyvillämielin luottaa $_SESSIONiin?
Keksiä taas tarvitaan vain jos käyttäjä vierailee pidempään ja sitä varten minulla on turvallisinta lisätä siihen 1) session ID, 2) käyttäjän ID sekä 3) satunnaisesti luotu pidemmän kirjautumisen avain. Oletan että riittää vain se, että tarkistan näiden kolmen tiedon täsmäävyyden tietokantaa vasten ja annan käyttäjän kirjautua sisään?
Tämä keksin kautta käyttäjän varmistaminen herättää eniten huolta lähinnä siksi, että pysyy teoriassa mahdollisena varastaa käyttäjän keksi ja päästä sillä kirjautumaan sisään. En kuitenkaan keksi mitään, millä parantaa suojautumista tätä vastaan, koska esimerkiksi IP-osoite saattaa heitellä paljonkin... käyttäjä vaihtaa vaikka ADSL-yhteydestä nettitikulle, tai yritysverkossa liikennettä ohjataan kuormituksen mukaan palvelimelta toiselle ja siten IP-osoitteen oikeanpuolimmaiset kaksi numeroa voivat vaihdella.
Merri kirjoitti:
Jos olen ymmärtänyt oikein, $_SESSION luodaan X minuutiksi PHP:n asetuksista riippuen ja sen kestoon ei kauheasti voi vaikuttaa mm. palvelimien ajamien sessioita poistavien cronien takia.
Voit kirjoittaa omat funktiot istuntojen käsittelyyn (session_set_save_handler), jolloin voit tallentaa istunnot vaikka omaan tietokantaasi eikä kesto ole enää palvelimen muusta toiminnasta riippuvainen. Silloin myös vanhentuneiden istuntojen poistaminen jää omalle vastuullesi. Lisäksi pitää tietenkin asettaa istunnon evästeelle aikaraja (session_set_cookie_params).
Merri kirjoitti:
Keksiä taas tarvitaan vain jos käyttäjä vierailee pidempään ja sitä varten minulla on turvallisinta lisätä siihen 1) session ID, 2) käyttäjän ID sekä 3) satunnaisesti luotu pidemmän kirjautumisen avain.
Jos tarkoitat istunnon ID:llä $_SESSION-muuttujaan liittyvää istuntoa, olet hakoteillä: eikö koko juttu lähtenyt siitä, että istunnot katoavat automaattisesti? Joka tapauksessa kohdat 1 ja 3 ajavat käytännössä samaa asiaa eli yksikin tunniste riittää. Jos lisäksi varmistat, että tunniste on yksilöllinen, kuten on viisasta, et varsinaisesti tarvitse myöskään käyttäjän ID:tä. Toki ID voi olla hyödyllinen esimerkiksi siinä tapauksessa, että istunnon vanhennuttua halutaan näyttää kirjautumislomakkeessa automaattisesti käyttäjän nimi. Toisaalta silloin myös keksin varastaja saa koneen käyttäjän ID:n selville, vaikka itse keksi olisi muuten jo vanhentunut.
Merri kirjoitti:
Tämä keksin kautta käyttäjän varmistaminen herättää eniten huolta lähinnä siksi, että pysyy teoriassa mahdollisena varastaa käyttäjän keksi ja päästä sillä kirjautumaan sisään.
Myös PHP:n istunnon ID tallennetaan evästeeseen. Sen varastaminen on siis yhtä helppoa kuin omatekoistenkin evästeiden. Lisävaaraa tulee siis ainoastaan siitä, että oma evästeesi on voimassa pidempään kuin PHP:n oletusarvoinen istuntoeväste eli varkaalla on enemmän aikaa toimia.
Jos et aio tallentaa istuntoon muuta kuin tiedon kirjautumisesta (käyttäjän ID:n), voit minusta jättää koko istunnon pois ja käyttää pelkästään tuota omaa evästettä. Näin säästyt tekemästä kahta eri systeemiä. Toisaalta jos istunnossa on muutakin tai evästeen tarkoitus ei ole säilyttää istuntoa vaan pelkästään päästää käyttäjä "kirjautumaan" ilman salasanaa, erilliset systeemit ovat perustellut.
Merri kirjoitti:
En kuitenkaan keksi mitään, millä parantaa suojautumista tätä [varastamista] vastaan, koska esimerkiksi IP-osoite saattaa heitellä paljonkin...
Yksi toteuttamiskelpoinen suojakeino on, että evästeessä oleva tieto vaihtuu usein. Jos istunto varastetaan kesken käytön, toinen saa muuttuneen tunnisteen ja toinen ei, jolloin käyttäjälle voi heti ilmoittaa varkaudesta ja istunnon voi katkaista. Jos istunto varastetaan muuna aikana, käyttäjälle voi kertoa seuraavalla käynnillä, että samaa istuntoa on käytetty toiselta koneelta, ja samalla voi vaikka näyttää, mistä kyseinen käyttö on peräisin.
Esimerkiksi Facebookista voi myös tilata sähköpostiin ilmoituksen, kun tilille kirjaudutaan uudella laitteella, ja profiilin asetuksissa on lista aktiivisista istunnoista.
Lisävarmistuksen tarve riippuu tietenkin myös palvelun sisällöstä. Tavallisella foorumilla en olisi kovin huolissani, tuskinpa kukaan juuri mitään menettää, vaikka käyttäjätiedot saisikin kalasteltua. Kuten aina, tärkeimmät turvakeinot ovat käyttäjän järki ja toimiva virussuoja sekä oikeasti tärkeissä asioissa salattu yhteys (HTTPS).
Käytännön toteutuksesta sen verran, että tosiaan tallenna keksiin esim. 128-bittinen vahva satunnaisluku ja tämän saman luvun sijoitat myös kantaan. Tässä on oleellista että tuo satunnaisluku on laadukas, tyyliin /dev/urandom tjms. matskua. Sitten jos käyttäjä tulee paikalle ilman voimassaolevaa istuntoa, mutta tuollainen "remember me" eväste löytyy (ja se löytyy myös kannasta), niin pyöräytät käyttäjälle istunnon käyntiin ihan kuten tunnuksella ja salasanallakin kirjauduttaessa.
Pidä huoli että yhdellä satunnaisluvulla voi pyöräyttää istunnon käyntiin vain kerran. Ja tällaisessa tapauksessa (kun käyttäjältä tulee tällaista syötettä) kun tarkistat tuota satunnaislukua, niin varmista ettet vuoda "aikainformaatiota" kun tarkastat vastaako käyttäjältä tullut satunnaisluku kannassa olevaa satunnaislukua. Käytännössä ==-vertailun sijaan vertaat merkkijonoja niin että väärän merkkijonon ja prikulleen oikean merkkijonon vertailu ottaa aikaa saman verran (googlaa constant time string comparison tjms.).
Tämä siis tuosta "remember me" toiminnosta yksinkertaistetusti, toki kaikki muut sessioon liittyvät ongelmat ja salatu yhteys, mitä tuossa ylempänä mainittiinkin, jne. liityvät myös oleellisesti tähän kokonaisuuteen.
Selvä, kiitoksia näistä. Jätän asian jossain määrin muhimaan aivokoppaan omillaan ja palaan asiaan kun eriytän kirjautumistoiminnot omaan luokkaansa. Lueskelin aiheesta myös PHP.netin password hashing -sivulla ja totesin, että suolausmenetelmäni lienee varsin riittävä:
Eli pidemmästä uniqid:n arvosta MD5 + samaisen satunnaisluvun 8 viimeistä numeroa. Tämä on sentään jo huomattavasti pidempi kuin esimerkiksi MyBB:n vastaava suolausavain, joka on vaivaiset 10 merkkiä (eli 40 bittiä).
Se minkä nyt kuitenkin toteutin on tuo usein vaihtuva automaattisen kirjautumisen avain, ja se vastaa myös siihen mietteeseen, kuinka ylläpitäjän kirjautumisen saa pidettyä turvallisena. Siihen lisään salasanavarmistuksen ja erillisen automaattisen kirjautumisen kaltaisen avaimen, mutta toki huomattavasti lyhemmällä aikamääreellä.
Projektini on tällä hetkellä kaiketi nimettävissä "epätavanomaiseksi foorumiksi". Ei vielä hajua tulevatko jotkut ajatukseni toimimaan käytännössä, koska toteutettu järjestelmä on kuitenkin aina hieman eri asia kuin suunnitelma pöydällä.
Merri kirjoitti:
Selvä, kiitoksia näistä. Jätän asian jossain määrin muhimaan aivokoppaan omillaan ja palaan asiaan kun eriytän kirjautumistoiminnot omaan luokkaansa. Lueskelin aiheesta myös PHP.netin password hashing -sivulla ja totesin, että suolausmenetelmäni lienee varsin riittävä:
Eli pidemmästä uniqid:n arvosta MD5 + samaisen satunnaisluvun 8 viimeistä numeroa. Tämä on sentään jo huomattavasti pidempi kuin esimerkiksi MyBB:n vastaava suolausavain, joka on vaivaiset 10 merkkiä (eli 40 bittiä).
Kannattaa käyttää vahvaa satunnaisuutta ihan salasanojen suolauksessakin, vaikka se ei vahvan satunnaisuuden tärkein käyttökohde olekaan. Tuollainen satunnaismerkkijonon kyhäys "ei vahvasta" satunnaisdatasta ei tee siitä lopullisesta merkkijonosta yhtään sen satunnaisempaa (vaikka sitä kuinka pyörittelisi ja venkslaisi).
Noihin "remember me" tokeneihin ja salasanojen suolaukseen saat satunnaisdatan lukemalla /dev/urandomia, tai käyttämällä PHP:n openssl_random_pseudo_bytes() tai mcrypt_create_iv() funktioita. Muu "satunnaisdata" on syytä jättää omaan arvoonsa eli unohtaa kokonaan (kun käyttötarkoitus on vähääkään kryptomaailmaan viittaava).
Toki hyvän funktion käyttö on fiksua, kun sellainen on olemassa. Kuitenkin realistisesti ajatellen satunnaislukujen vahvuus ei tässä käytössä merkitse juuri mitään, jos brute-force-hyökkäykset on estetty automaattibannauksella. Nyt puhutaan sentään evästeessä olevasta tiedosta, jota käyttäjä ei voi edes vahingossa kirjoittaa väärin, joten voi aika huoletta laittaa pitkätkin bannit jo kahden peräkkäisen virheen jälkeen.
MD5 ei tuo yllä esitettyyn koodiin olennaista lisäarvoa, vaikka se ehkä hienolta ja kryptografiselta näyttääkin. Kuten timoh sanoo, jos yksinkertainen uniqid(1,1) ei riitä, kannattaa saman tien tehdä kunnon satunnaisdataa valmiilla funktiolla tai muuten tunnetulla ja analysoidulla menetelmällä.
Päädyin nyt seuraavaan satunnaista Base64:ää ulostavaan pätkään:
$out = ''; if( @is_readable('/dev/urandom') ) { $f = fopen('/dev/urandom', 'r'); $urandom = fread($f, $length); fclose($f); for($i = 0; $i < $length; $i++) { $code = 48 + (ord($urandom[$i]) % 64); if($code > 109) { $out .= chr($code - 65); } elseif($code > 83) { $out .= chr($code + 13); } elseif($code > 57) { $out .= chr($code + 7); } else { $out .= chr($code); } } } else { mt_srand(time() % 2147 * 1000000 + (double)microtime() * 1000000); for($i = 0; $i < $length; $i++) { $code = 48 + (mt_rand() % 64); if($code > 109) { $out .= chr($code - 65); } elseif($code > 83) { $out .= chr($code + 13); } elseif($code > 57) { $out .= chr($code + 7); } else { $out .= chr($code); } } } return $out;
PHP.netissä olevien kommenttien lueskelun perusteella openssl_random_pseudo_bytes() on ilmeisesti osassa Windows-koneista sikamaisen hidas ja siten käyttökelvoton (aiheuttaen timeoutteja), ja hitaus vaivaa myös mcrypt_create_iv():tä koneesta riippumatta (yli kolmen sekunnin sivulatauksia). Kehityskoneeni on Windows, joten /dev/random ei ole saatavilla.
Tuo koodi näyttää hieman epäilyttävältä, ja ettei jopa kadota satunnaisuutta matkan varrella. Herääkin kysymys miksi et vaan käytä valmiita funktioita kuten base64_encode() tai bin2hex() ;) Tietysti tuollaiset muutokset pidentävät merkkijonoa, mutta se tuskin ongelmaksi muodostuu.
Nuo ongelmat openssl_random_pseudo_bytes() ja mcrypt_create_iv() funktioissa on korjattu hyvän aikaa sitten, joten jos vaan et käytä vanhentuneita PHP-versioita, niin ongelmaa ei ole (tiettävästi). Tosin tarkkana saa olla senkin takia ettei nuo funktiot suolla "ei vahvaa" satunnaisdataa, mutta tämäkin ongelma poistuu kunhan vain käytät uusia PHP:n versioita.
Tuo mt_rand() kuuluu samaan sarjaan uniqid:n kanssa, älä käytä.
Pikkuisen vielä aiheen vierestä liittyen tuohon Metabolixin mainitsemaan bannaukseen. Se nimittäin pohjimmiltaan kääntyy itseään vastaan. Ongelmaa on että mitä pitäisi bannata, IP vai ehkä käyttäjätili? Esim. kiusantekijä aiheuttaa tilanteen missä käyttäjiä häiritään niin että heidät saadaan bannatuiksi, eli siis ns. palvelunestotilanne. Tuolla taisin jotain asiasta mainita aiemmin https://www.ohjelmointiputka.net/keskustelu/
Itse mietin tilannetta, jossa tunniste on jo poistettu tietokannasta mutta käyttäjän selain lähettää sitä vielä. Jos tästä tulisi banni niin ei hyvä.
Tuo funktio tosiaan käytännössä tekee base64_encoden, seurausta myöhään koodaamisesta ja pohjaamisesta jonkun toisen käyttämään koodiin. En myöskään vielä läheskään aina muista, että PHP:ssa on valmis funktio melkein kaikkeen.
Bannausjärjestelmän pohdinta on asia erikseen... olen ollut jossain määrin tyytymätön niihin keinoihin, joita foorumisoftissa on ollut tarjolla. Esimerkiksi IP-pohjainen banni on melkein yhtä tyhjän kanssa, ellei sitten koeta nähdä vaivaa seurata session kautta kaikkia käytettyjä IP-osoitteita ja bannaa niitä kaikkia sitten kun ongelmat alkavat, mutta se on kömpelöä ja jossain määrin vaarallistakin. Aina kun syntyy mahdollisuus bannata myös käyttäjiä, joita ei ollut tarkoitus bannata. Samoin jos eväste tuhotaan tai jätetään huomioitta kokonaan, niin sessiopohjainen seuranta ei edes toimi.
mcrypt_create_iv
tuntui olevan ihan nopea, mutta tuntui generoivan Blowfishille erittäin lyhyen IV:n. Openssl_random_pseudo_bytes
tuntuu kyllä vieläkin erittäin hitaalta vertailtaessa PHP 5.4:llä Windowsilla. Itse tein vähemmän kriittiseen sovellukseen salasanan hashauksen tyyliin.
<?php $salt = str_shuffle(uniqid(mt_rand(), true)); $options = '$6$rounds=5000$' . $salt . '$'; crypt($password, $options); ?>
Kryptografisesti ajatellen tuosta voisi varmasti jättää str_shufflen
ja mt_randin
pois, palvelevat vain semiturhaa obfuskointia. Heitin vielä koodin kommentteihin, että parempaa satunnaisdataa saisi tosiaan /dev/randomista (tai urandomista) ja ssl- sekä mcrypt-funktioilla. Suolahan noissa cryptin
algoritmeissa on melko lyhyt, tuossa SHA-512:n tapauksessa 16 merkkiä, Blowfishissa 22 merkkiä.
Useammassakin Stackoverflow:n keskustelussa kehotetaan käyttämään PHPassia.
Blowfishin blokin koko on vain 64 bittiä, tämän takia varmaan tuo kun sanoit että tuntui generoivan lyhyen IV:n? Meinaan siis että käytit mcrypt_get_iv_size():ä tjms.
Liekö sitten taas onnistuttu ryssimään Window-versiossa openssl_random_pseudo_bytes() PHP 5.4:llä, nimittäin 5.3.4:ään tuo blocking aikoinaan fixattiin.
/dev/random:ssa tuota blokkausta taas on, ihan tarkoituksella, ja sen takia sitä ei tulisi käyttää, ainakaan webbimaailmassa, ikinä. /dev/urandom on aivan vastaava käytännössä (ja oikeastaan teoriassakin), mutta se ei ikinä blokkaa.
Tosiaan phpass on hoitaa homman ja jos käytät nykyaikaista PHP:tä, niin ei ole huolta että phpass joutuu generoimaan fallback hasheja. Käytännössä PHP:llä salasanoja ei pitäisi käsitellä millään muulla kuin CRYPT_BLOWFISH:llä tai CRYPT_SHA512:lla (ainakaan tällä hetkellä, kun parempaa ei ole saatavilla).
Aihe on jo aika vanha, joten et voi enää vastata siihen.