BLOG

Uusimmat kuulumiset ohjelmistotalosta

Tietoturvalliset verkkosovellukset - osa 4

Ohjelmointi, Turvallisuus, Antti Sand

Tämä kirjoitus on suoraa jatkoa edellisille kolmelle kirjoitukselle turvallisista verkkosovelluksista. Tämä kirjoitus pyrkii nitomaan kasaan sellaisia aiheita, joihin olen jo viitannut, mutta en vielä käsitellyt. Tämän kirjoituksen aiheina ovat brute force -hyökkäys, XSS, CSRF, defensiivinen ohjelmointi, sekä turvallisuus obfuskoinnilla.

Tämä kirjoitus on osa tietoturvalliset verkkosovellukset -sarjaa. Löydät kaikki sarjan kirjoitukset tästä linkistä.

Tämä kirjoitus koostuu lyhyistä, spesifeistä katsauksista vaihteleviin aiheisiin, jotka kuitenkin kaikki liittyvät osaltaan tietoturvallisten verkkosovellusten suunnitteluun. Osa asioista on luonteeltaan reaktiivisia, eli miten vastataan johonkin uhkaan, osa taas enemmän ideologisia - ajatuksia siitä mentaliteetista, joka ohjelmistokehittäjällä tulisi olla.

Brute force -hyökkäys

Brute force -hyökkäyksessä ei pyritä murtamaan salasanoja, löytämään tietoturva-aukkoja, tai "krakkeroitumaan" mihinkään. Siinä yritetään vain systemaattisesti arvata käyttäjän salasana. Käyttäjätunnus on yleensä helppo päätellä, sillä usein käyttäjätunnuksena käytetään vain sähköpostiosoitetta. Tämä on ihan hyvä tapa, sillä sähköpostiosoite on helppo muistaa ja se on luonteeltaan yksilöllinen.

Kun käyttäjä yrittää kirjautua tililleen virheellisillä tiedoilla, järjestelmä kyllä tietää oliko käyttäjätunnus vai salasanan virheellinen, vai molemmat. Usein tämä tieto pimitetään käyttäjältä sillä perusteella, että jos kirjautuja saa tietää, että käyttäjätunnus oli oikein, mutta salasana väärin, niin silloin hyökkääjä tietää edes toisen tarvittavista tiedoista. Tämän taas ajatellaan helpottavan brute force -hyökkäyksiä. Mutta jos brute forceen on varauduttu kunnolla, niin ei ole kovin hyviä perusteita huonontaa käyttökokemusta antamalla tarpeettoman epätarkka virheilmoitus.

Brute force -hyökkäykseen voidaan varautua pitämällä kirjaa käyttäjätunnukselle väärin syötettyjen salasanojen määrästä, eli siis vihreellisten kirjautumisyritysten määrästä. Tämä voi olla osa käyttäjätietomallia suoraan tai pivot -taululla siihen liitetty. Näin joka kerta, kun kirjaudutaan virheellisesti, lisätään kentän arvoa. Sitten kun arvo saavuttaa jonkin rajan, laitetaan tietomalliin ylös, että tili on lukittu tietyksi ajaksi. Yksi yleinen tapa on lukita tili 60 sekunniksi kolmen virheellisen kirjautumisen jälkeen. Nämä luvut ovat tietysti mielivaltaisia, mutta jo minuutin odotus kolmen yrityksen jälkeen saa brute force -hyökkäyksen vaatiman ajan nousemaan liian korkeaksi. Ja tämä on reaalimaailman aikaa, johon ei Mooren laki vaikuta.

Jos kyseessä on todella sensitiivinen järjestelmä, voitaisiin tili lukita kokonaan ja vaatia käyttäjää olemaan yhteydessä tukeen saadakseen tilinsä jälleen auki. Normaalitilanteessa tämä on kuitenkin yliampuvaa.

Ajatusta voisi jatkaa kirjaamalla virheellisten pyyntöjen IP-osoitteen ja estämällä kaiken liikenteen ohjelmaan tuosta osoitteesta, mutta usein isot organisaatiot saattavat jakaa muutaman IP-osoitteen, jolloin laillisilta käyttäjiltä voi mennä pääsy samalla.

Vielä yhtenä ajatuksena voisimme tarkistaa, että tuleeko yhteydenotto sellaisesta IP-osoitteesta, joka on geograafisesti jossain ihan muualla, kuin yleensä käyttäjän yhteydenotot. Näin voitaisiin tehdä oletuksia, että joku kenties yrittää luvatta tilille. Tämä kuitenkin tänä maailmankansalaisuuden aikana saattaa tuoda enemmän haittaa, kuin hyötyä.

Joka tapauksessa, jos monille tileille alkaa tulemaan viheellisiä kirjautumisia luonnottoman nopeaan tahtiin samasta IP-osoitteesta, se voidaan turvallisin mielin sulkea.

Cross Site Scripting

Cross Site Scripting lyhennetään yleensä XSS. Periaatteena tässä on saada haitallista koodia ajettua laillisen käyttäjän istunnossa. XSS hyökkäykset voidaan jakaa hetkellisiin ja pysyviin. Hetkellinen XSS hyökkäys voisi olla vaikkapa tilanteessa, jossa sivulle voidaan antaa HTTP GET -parametreja, query string -parametreja. Tällaisia voisivat olla vaikkapa:

http://www.mysite.com/?category=Funny&per_page=50

Jos näitä parametreja ei tarkisteta, niiden kautta voidaan syöttää haitallista koodia sivulle. Jos käyttäjä sitten jonkin sähköpostilinkin kautta ohjataan tuollaiselle sivulle, niin järjestelmä tunnistaa käyttäjän normaalisti, aloittaa istunnon ja luo tarvittavat käyttöoikeudet. Nyt haitallisella koodilla on vapaa pääsy kaikkiin niihin toimintoihin, joihin kirjautuneella käyttäjällä on oikeus.

Pysyvä XSS hyökkäys puolestaan käyttää järjestelmään tallentuvaa dataa. Tämä on periaatteessa sama asia, jota jo ensimmäisessä sarjan kirjoituksessa käsiteltiin. Jos joku saa haitallisen JavaScript -koodin upotettua vaikkapa blogin kommenttikenttään, se ajetaan kaikkien blogia selaavien selaimissa.

Varautuminen XSS -hyökkäyksiin

Varautuminen tähän on sama, kuin sarjan ensimmäisessä osassa. Älä koskaan luota käyttäjiisi. Tarkemmin, älä koskaan luota mihinkään sellaiseen sisältöön, joka tulee järjestelmän ulkopuolelta. Kommenttikentän teksti on ilmeinen syyllinen, mutta joskus saattaa unohtua kuvien XIFF -tietueet, järjestelmään syötettävät CSV -tiedostot, tai muut ladattavat tiedostot, joiden sisältö näytetään sivulla muodossa tai toisessa.

Cross Site Request Forgery

Cross Site Request Forgery lyhennetään yleensä CSRF. Se on XSS:n vastapuoli. Siinä, missä XSS:ä yritetään ajaa tietoja luotetun sivun avulla, CSRF:ssä yritetään ajaa tietoja luotetun käyttäjän avulla. Esimerkkinä voisi olla vaikkapa sähköpostilinkki, joka vie käyttäjän siihen HTTP -reittiin, joka vastaa järjestelmässä vaikkapa kirjautuneen käyttäjän tilin poistamista. Koska käyttäjä on tunnettu, järjestelmä tekee mitä pyydetään. Ja koska järjestelmiä kirjoitetaan noudattamaan RESTful reititystä, ei ole vaikea arvata tuon HTTP -reitin muotoa.

Ja hyökkäyksen ei tarvitse tulla tekstilinkin kautta. Se voi olla kuva, painike tai automaattisesti ajettu JavaScript -uudelleenohjaus. REST:n mukaan tilin poiston tulee tapahtua HTTP POST -kutsulla, mutta ei se paljoa vaatisi saada CSRF -hyökkäys tekemään nimenomaan HTTP POST -kutsun.

Varautuminen CSRF -hyökkäyksiin

Ensimmäinen asia, joka pitäisi RESTful -metodologiassa olla itsestään selvä, on se, että tietoja muuttavia reittejä ei ikinä kuunnella HTTP GET -verbeinä. GET on vain tietojen lukemista varten.

Tämä ei kuitenkaan riitä, sillä kyllä niitä POST -kutsujakin voidaan luoda vaikkapa JavaScriptin kautta. Tarvitaan jokin asia, joka varmistaa, että pyyntö on tullut meidän sivuilta, meidän oman "poista tili" -painikkeen kautta. Tähän voimme käyttää Nonce token:ia. None tulee sanoista Number user Once, eli "token" on jokin avain, jota voidaan käyttää vain kerran. Ts. uusi sellainen luodaan jokaista kutsua varten. Tätä kertakäyttöavainta käytetään sitten jokaisessa laillisessa lomakkeessa piilokenttänä ja se sama kertakäyttöavain tallennetaan istuntoon yhden pyynnön ajaksi. Sitten, kun pyyntö tilin poistamiseksi käsitellään, tarkistetaan, että lomakkeessa oli sama kertakäyttöavain, kuin istunnossakin.

Jos hyökkäys tulee ulkopuolelta, siinä ei voi olla samaa kertakäyttöavainta, joten voimme huoletta sivuuttaa koko pyynnön. Tai oikeammin kirjata muistiin, että hyökkäystä on yritetty.

PHP:ssa tämä saattaisi näyttää seuraavalta:

Route::get('/signup', function() {
	// Luodaan jokin sattumanvarainen avain
	$token = generateCsrfToken();

	// Asetetaan se istuntoon yhden pyynnön ajaksi
	Session::flash('token', $token);

	// Ja viedään sama avain myös lomakkeelle
	$data['token'] = $token;

	return render('signup_form', $data);
});

Lomakkeessa puolestaan meillä olisi vain yksi ylimääräinen piilokenttä:

<input type="hidden" name="_token" value="<?=$token?>">

Lopulta, kun pyyntö saapuu takaisin palvelimelle, tarkistetaan, että avaimet vastaavat toisiaan:

Route::post('/signup', function() {
	if($_POST['_token'] === Session::get('token')) {
		// Voidaan tallentaa tietoja
		...
		return "Tiedot tallennettiin";
	}

	return "Laiton toiminto";
});

Defensiivinen ohjelmointi

Tämä ei ole mikään metodi tai kirjasto, vaan mielentila ja toimintatapa. Defensiivisessä ohjelmoinnissa kantavana ajatuksena on älä koskaan luota itseesi. Itse asiassa yllä olevassa koodinpätkässä on jo pieni esimerkki defensiivisestä ohjelmoinnista. Kyseinen metodi palauttaa "epätoden" arvon aina paitsi silloin, jos kaikki ehdot täyttyvät. Epätosi arvo tässä tapauksessa on vain ilmoitus, että toiminto oli laiton.

Sen sijaan, että tekisimme jotain aina, paitsi silloin, jos käyttäjällä ei ole oikeutta toimintoon, emme tee koskaan mitään, paitsi silloin, jos käyttäjällä on oikeus toimintoon. Ajattelemme siis kaiken päin vastoin.

Näin pienellä ajattelutavan muutoksella saamme rakennettua lisäkerroksen turvallisuuteen. Kaikki tietenkin kumpuaa siitä, että oikeudet tarkistaminen saattaa joskus unohtua ohjelmoijalta. Tai unohtuu se, että mitä kaikkea muutakin piti tarkistaa, kuin vain se, että käyttäjä on kirjautuneena. Onko käyttäjällä silti oikeutta tähän toimintoon? Jos ajatellaan, että mikään ei ole sallittua, ellei ehdot täyty, niin päädymme virhetilanteissa siihen, että käyttäjä ei saa tehtyä asiaa, johon hänellä on oikeus. Tietoturvan kannalta katson tämän kuitenkin olevan parempi kuin se, että käyttäjä saisi tehtyä jotain, johon hänellä ei ole oikeutta. Näin päin virhe on myös huomattavasti helpompi havaita. Moni käyttäjä soittaa vihaisena, jos ohjelma ei toimi. Harva hyökkääjä puolestaan soittaa siitä, jos hyökkäys onnistui.

Turvallisuus obfuskoinnilla

Security through obscurity. Turvallisuutta obfuskoinnilla. Harvoin totta, mutta joskus hyödyllistä. Obfuskointi, eli asioiden tarpeettoman epäselväksi tekeminen, on joskus saattanut olla syy, miksi Linux -käyttöjärjestelmän tietoturva-aukot on havaittu ja paikattu nopeammin, kuin Windowsin. Ajatuksena on ollut se, että jos kukaan ei tiedä, miten koodi toimii, niin siitä on vaikeampi löytää haavoittuvuuksia. Vasta-argumentti on tietenkin se, että jos kaikki tietävät miten koodi toimii, siitä löydetään - ja paikataa - haavoittuvuuden nopeammin.

Nykynäkemys on se, että avoimuus on hyvä asia. Obfuskaatiolla on silti käyttönsä. Ajatellaan vaikka että sivulla listataan jotain ulkoisia resursseja (vaikkapa PDF -tiedostoja), joista käyttäjällä on oikeus avata vain tietty osa. Järjestelmän suunnittelija ottaa oikeudet huomioon ja listaa sivulla linkit vain niihin tiedostoihin, joihin käyttäjällä on oikeus. Samalla kuitenkin, välttääksen tiedostojen nimiristiriidat ja laittomat merkit tiedostojen nimissä, ohjelmoija on todellisuudessa tallentanut tiedostot juoksevalla numeroinnilla. Tiedostojen alkuperäiset nimet puolestaan ovat tietokannassa samalla tavalla juoksevalla numeroinnilla.

Nyt jos tiedoistoihin pääsyä ei ole muuten estetty, niin hyökkääjän tarvitsee vain kokeilla hyvin loogisesti jatkuvalla numeroinnilla HTTP GET -pyyntöjä samaan osoitteeseen, mistä ne sallitut tiedostot tulivat, saadakseen käsiinsä ne kaikki muutkin. Tietysti turvallisessa verkkosovelluksessa tiedostot eivät ole suoraan saatavilla missään kansiossa, vaan järjestelmä hakee ja tarjoilee ne itse tarkistaen samalla oikeudet. Mutta monissa paikoissa näkee silti esimerkin kaltaisia toteutuksia. Kehittäjän kannalta juokseva numerointi on kätevä, sillä sitä voidaan suoraan käyttää tietokannan pääavaimena.

Jotta seuraavan resurssin nimi ei olisi suoraan arvattavissa, juoksevan numeroinnin voisi vaihtaa sattumanvaraiseksi merkkijonoksi. Tällöin hyökkääjän olisi hyvin vaikea arvata muiden resurssien nimiä. Varsinaista tietoturvaa tämä ei tarjoa, mutta näkösuojaa kuitenkin. Kenties emme halua näyttää käyttäjälle, että hän on järjestelmämme seitsemäs käyttäjä.

Obfuskointi ei tuo suoraan tietoturvaa, mutta joissain tilanteissa sen käyttö on perusteltua.

Käyttäjätilit ja käyttöoikeudet

Useinkaan verkkosovelluksissa ei riitä se, että tiedämme pyynnön tulevan oikealta käyttäjältä. Yleensä haluamme tehdä lisää tarkistuksia varmistuaksemme siitä, että käyttäjällä on oikeus kyseiseen toimintoon. Tämän toimintamallin toteuttamiseksi on monia vaihtoehtoja. Joissain tapauksissa rooleihin ja roolien oikeuksiin perustuva malli voi olla paras. Käyttäjällä on yksi tai useampi rooli ja kullakin roolilla on joukko oikeuksia. Näin ylläpitäjän ei tarvitse mikromanageroida jokaisen yksittäisen käyttäjän yksittäisiä oikeuksia, vaan käyttäjälle voidaan asettaa, ja ottaa pois, jokin rooli.

Eri rooleilla on tietysti erilaisia oikeuskia ja yksittäisen käyttäjän oikeudet saadaan katsomalla kaikkien käyttäjän roolien oikeudet. Koska rooleja ja oikeuksia voi olla useita ja usein eri rooleilla voi olla päälekkäisiä oikeuksia, saattaa kehittäjälle tulla houkutus tallentaa käyttäjän yksittäiset oikeudet vaikkapa kyseiseen istuntoon. Näin ne olisivat aina nopeasti saatavilla, eikä niitä tarvitsisi aina hakea uudelleen yhdistelemällä tässä tapauksessa kolmen taulun, user, role ja permission, sekä kahden pivot-taulun, role_user ja permission_role, tietoja. Tässä on kuitenkin se huono puoli, että jos käyttäjältä otetaan jokin rooli, tai käyttäjän roolilta jokin oikeus pois kesken istunnon, muutos ei välity käyttäjälle ennen seuraavaa kirjautumiskertaa.

Tämä on siis kysely, joka pitäisi tehdä joka pyynnön yhteydessä. Onneksi tietokannat ovat nopeita.

Yhteenveto

Tässä kirjoituksessa käsiteltiin lyhyesti sellaisia asioita, joihin olin jo aiemmin viitannut, mutta joita en ollut vielä täysin avannut. Lyhykäisyydessään tämä nitoi muutamia irtonaisia tarninan kaaria yhteen. Tietoturvallisuudesta tässä ei kuitenkaan tarjottu tyhjentävää vastausta. Muita kirjoittamisen arvoisia asioita olisivat vielä esimerkiksi kertakäyttösalasanat tekstiviestillä kännykkään, tunnuslukukortit, tunnistautuminen rajapinnoissa ja RFID -tunnistaminen.

Lähteet ja lisää luettavaa

Building secure PHP apps - a practical guide, Ben Edmunds

Antti Sand

Ikuinen oppija, kiinnostunut miltei kaikesta, paitsi jääkiekosta. Siltä osalta siis aivan väärässä seurassa. Kirjoittelee joskus pohditojaan oppimisesta tähän blogiin.

Takaisin blogisivulle