Älä koskaan luota käyttäjiisi
Aloitetaan opettavaisella tarinalla. Sarjakuvan muodossa (Jee!):
XKCD - Little Bobby Tables. Lyhyt kuvaus sarjakuvan tapahtumista on siis se, että käyttäjä syötti nimensä lomakkeeseen. Tärkeä ominaisuus olla olemassa. Nimi vain sattui olemaan "Robert'); DROP TABLE students;--"
.
Se, mitä tässä tapahtui oli nimeltään SQL Injektio.
SQL Injektio
Järjestelmä saattoi kyseisessä tilanteessa olla vaikkapa seuraavan kaltainen:
mysql_query('UPDATE users
SET first_name="' . $_POST['first_name'] . '"
WHERE id = 1001');
Ja kehittäjä kenties oletti muodostuvan lausekkeen olevan tällainen:
UPDATE users SET first_name = "Robert" WHERE id=1001;
Mutta koska kehittäjä luotti käyttäjiin, eikä tarkistanut syötteen kelvollisuutta, päätyi lopulliseksi komennoksi taulun pudottaminen. Samanlainen, huvittavampi variaatio olisi ollut kirjoittaa nimekseen Sami", last_name="Sammakko"; --
, jolloin lopullinen kysely olisi ollut:
UPDATE users
SET first_name="Sami", last_name="Sammakko"; --"
WHERE id=1001;
Joka olisi kirjoittanut jokaisen käyttäjän nimeksi Sami Sammakko. Huomaa, että ensimmäinen kysely päättyy kohtaan ;
ja WHERE id=1001;
jää ylimääräiseksi, virheelliseksi syntaksiksi.
Tätä haavoittuvuutta kutsutaan SQL Injektioksi, eli haitallisen SQL komennon ujuttamista ajettavan komennon sisään.
Huvittavaa, mutta ei kai tällaista koskaan tapahdu?
SQL Injektioita kautta historian
Useimmiten näiden tapausten yksityiskohtia ei paljastettu, joten osa saattaa olla muidenkin tieoturvaongelmien aiheuttamia, mutta joidenkin asiantuntijoiden mukaan nämä johtuivat pääosin SQL Injektio-haavoittuvuuksista.
- LindedIn vuoti kuuden miljoonan käyttäjän tiedot vuonna 2012
- Yahoo! vuoti 450 000 käyttäjän salasanat vuonna 2012
- Nvidia vuoti 400 000 salasanaa vuonna 2012
- Adobe vuoti 150 000 salasanaa vuonna 2012
- eHarmony vuoti 1 500 000 salasanaa vuonna 2013
[Ben Edmunds, 2014]
Ei siis ole aivan sanomatonta, etteikö tällaista voisi tapahtua tai tapahtuisi. Ja mikä huomattavampaa, pääosin asiantuntijayritysten toimesta.
Miten varautua SQL Injektiota vastaan
Helposti. Älä koskaan luota käyttäjiisi. Jos käytät puhdasta PHP:ta, hyödynnä PDO-luokan metodeja. Sanitize
, escape
, bind parameters
. PDO-luokan metodit varmistavat, että syöte on sanitoitua, varmistettua ja kunnollisilla lainausmerkeillä varustettua ja että se sisältää vain yhden arvon.
Edellinen lyhyt kysely oltaisiin voitu kirjoittaa PDO-luokan avulla seuraavasti:
$db = new PDO(...);
$query = $db->prepare('UPDATE users
SET first_name = :first_name
WHERE id = :id');
$query->execute([
':id' => 1001,
':first_name' => $_POST['first_name']
]);
SQL -tietokannat tarjoavat myös mahdollisuuden tallentaa kyselyitä stored procedures
-nimellä. Koska kysely on valmiiksi tietokannassa, eikä koko kyselyä tuoda ohjelmasta, ne ovat tietoturvallisempia, kuin ohjelmassa kirjoitetut kyselyt. Niillä on kuitenkin huomattavia haittapuolia:
- Vaikea testata
- Hajauttavat toimintalogiikan koodiin ja tietokantaan
- Vaikea saada osaksi versiohallintaa
- Vaativat jonkin verran asiaan vihkiytymistä
Teet sitten niin tai näin, niin yksi asia on syytä pitää mielessä. Selainpuolen validointi JavaScriptillä ei ole riittävä validointi. Sitä voi ja on suositeltavaa käyttää sellaiseen validointiin, jonka tarkoitus on antaa käyttäjälle parempaa palautetta ja ohjeita, mutta sen kiertäminen haittatarkoituksessa vaatisi kirjaimellisesti yhden napin painalluksen.
Useimmissa kehittyneissä ohjelmistokehyksissä tietokantakerros on kuitenkin abstraktoitu ja useimmiten voimme luottaa siihen, että niiden alla olevat tietokantakäskyt ovat melko turvallisia.
Massaosoitukset
Massaosoitukset, mass assignments
, voivat olla kehittäjän paras kaveri. Olioinstanssien alustaminen tapahtuu käden käänteessä, kun olion attribuuttien arvot voidaan asettaa kaikki kerralla.
Kuvitellaan, että meillä on sovelluksessa lomake:
<form action="...">
<input name="first_name" />
<input name="last_name" />
<input name="email" />
</form>
Tällä lomakkeella voidaan päivittää käyttäjä-olion instanssin attribuutit kaikki kerralla. Eloquentia käyttäen se saattaisi mennä vaikkapa näin:
$user = User::find(1);
$user->update(Input::all());
Nopeaa ja näppärää. Jos käyttä-oliolla on attribuutit first_name
, last_name
ja email
, niin nuo samat nimet löytyvät myös lomakkeeltamme ja ne voidaan massaosoittaa kaikki kerralla. Mutta missään kohtaa emme tarkista, ettei käyttäjä ole lisännyt kehityskonsolin kautta lomakkeeseen useampia kenttiä. Esimerkiksi hän olisi voinut lisätä meidän lomakkeeseemme oman kenttänsä:
<input type="hidden" name="permissions" value="{admin: true}" />
Nyt jos käyttäjä-oliolla olisi ominaisuus permissions
ja olisimme sattuneet nimeämään pääkäyttäjäominaisuuden nimellä admin
, niin nyt lomakkeen täyttäjä olisi tehnyt käyttäjätilistään pääkäyttäjätilin. Koska me annoimme siihen mahdollisuuden.
Miten varautu massaosoitus-haavoittuvuuteen
Helposti! Siihen on muutama vaihtoehto:
- Älä salli massaosoituksia ollenkaan
- Salli massaosoitukset vain etukäteen määrittelemiisi kenttiin (whitelist)
- Salli massaosoitukset kaikkiin muihin kenttiin, paitsi niihin, jotka olet etukäteen määritellyt (blacklist)
Tässä kävimme läpi sen syötteen tarkistamista, joka tulee järjestelmään sisään. Yhtä tärkeää on myös sen syötteen tarkistaminen, joka menee järjestelmästä ulos.
Sisään tuleva syöte voi vahingoittaa palvelintamme (paha juttu), ulos menevä syöte voi vahingoittaa asiakkaitamme (aika paha sekin).
Ulos menevän syötteen varmistaminen
Ei riitä, että puhdistat sisään tulevan datan, myös käyttäjälle menevä data tulee puhdistaa. Jos sisään tulevassa datassa ongelmana oli lähinnä haitallisen SQL:n ajaminen tietokannassa, ulosmenevän datan kanssa isoin riski on käyttäjän selaimessa ajettava JavaScript -koodi.
Ajatellaan vaikka, että sivustolla on kommentointiominaisuus. Siis kenttä, johon käyttäjä voi kirjoittaa oman kommentin. Järjestelmä puhdistaa syötteen turvalliseksi tietokantaa varten, mutta - jos tilanteeseen ei varauduta - palauttaa sen sellaisenaan jokaiselle sivulla vierailevalle. Otetaan yksinkertainen esimerkki siitä, miksi tämä voi olla huono juttu:
<script>alert('Minua ei suodatettu, joten ohjaanpa selaimesi mieskuntolääkekauppaan!');</script>
Koska syötteessä on suoraan <script>
-elementti, se ajetaan selaimessa oletuksena heti, kun merkkijono on saapunut palvelimelta. Alert olisi raivostuttava, mutta melko harmiton. Sen sijaan window.location
sisältää metodit sivun uudelleenohjaukselle. Eli kappas vain, tulit ohjatuksi haitalliselle sivulle, vaikka vierailit mielestäsi turvallisella sivustolla.
Toinen tapa ujuttaa haitallista syötettä käyttäjien selaimiin on hieman vähemmän tunnettu: XIFF. Jos käyttäjät voivat ladata kuvia sivustollesi ja näytät käyttäjille selaimessa noiden kuvien XIFF -tietueita, voidaan sinne laittaa mukaan haitallista koodia. Eli nekin tulee suodattaa.
Jos käytät ohjelmistokehystä, se saattaa hoitaa sanitoinnin puolestasi. Esimerkiksi Blade
tarjoaa syötteen tulostamiselle {{ $turvallinen }}
ja {!! $ei_turvallinen !!}
-syntaksin. Kaikki käyttäjältä tuleva syöte kuuluu ensimmäiseen ja vain oma koodisi (esim. JavaScript) kuuluu jälkimmäiseen.
Saman asian voi kuitenkin tehdä ihan perus PHP:lla. Meillä on kaksi natiivia funktiota syötteen sanitoimiseen: htmlentities()
ja htmlspecialchars()
. Molemmat muokkaavat tulostettavaa syötettä tehden siitä turvallisempaa. htmlspecialchars()
on varmaankin se funktio, jota käytetään 90% ajasta. Se etsii syötteestä sellaisia merkkejä, joilla on syktaktista merkitystä (esim. <
, >
, &
) ja muuntaa ne vastaaviksi HTML entiteeteiksi. htmlentities()
on kuin htmlspecialchars()
, mutta paljon innokkaampi. Se etsii kaikki sellaiset merkit, joilla on HTML entiteettipari. Eli esimerkiksi skandinaavisen kirjainmerkit. Tämä ei välttämättä ole se, mitä haluat. Skandien muuntaminen HTML entiteeteiksi voi vaikuttaa kätevältä tavalta välttää merkistöongelmia, mutta silloin teet asioita vaikeamman kautta. Ymmärrä näiden ero ja valitse tilanteen mukaan paremmin sopiva vaihtoehto.
Konsoliin kirjoittaminen
Se, että verkkosovellusta voidaan ohjata suoraan komentoriviltä on todella näppärää. Sillä tavalla voidaan ajaa esimerkiksi Cronissa suoraan järjestelmän kometoja, esimerkiksi tehdä jotain raskaita ylläpitotoimia ajastetusti. Samaten monimutkaisten kokonaisuuksien automatisointi yhden konsolikomennon taakse on huippua. Tässä tulee kuitenkin vastaan vielä vakavampi haavoittuvuus. Mitä jos käyttäjän haitallinen syöte tulostuukin suoraan komentoriville tuotantopalvelimellasi?
PHP tarjoaa tähänkin valmiit funktiot: escapeshellcmd()
ja escapeshellargs()
. Näiden avulla komentorivillä tapahtuvat komennot eivät avaa mahdollisuutta ajaa ylimääräisiä komentoja.
Yhteenveto
Tämä oli vasta ensimmäinen luku melko suureen kokonaisuuteen. Kävimme läpi sisääntulevan ja ulosmenevän syötteen varmentamisen. Jo tällä yhdellä periaatteella (älä koskaan luota käyttäjään) saadaan kaikkein helpoimmat tietoturva-aukot tukittua. Tietoturvalliset verkkosovellukset varautuvat näihin, mutta näiden lisäksi vielä moneen muuhunkin uhkaan. Seuraavissa osissa jatkamme aiheen parissa.
Lähteet ja kirjallisuutta
XKCD - Little Bobby Tables - Sarjakuva SQL Injektiosta
Fundamentals of Database Systems, Ramez Elmasri, Shamkant Navathe
Building secure PHP apps - a practical guide, Ben Edmunds