A PHP programozási nyelv

Biztonság

Bevezetés

A PHP nyelv a sikerét az egyszerűségének köszönheti. Sokak szerint emiatt PHP-ban nem is lehet szép programot (rendszert) fejleszteni. A fejlesztők a fejlesztés első néhány évében sok olyan tervezői döntést hoztak, melyek a nyelv egyszerűségét fokozták, ám ezzel rengeteg hibalehetőség került be a nyelvbe. A PHP beállításai között több olyan is van, melyek ezeket a lehetőségeket korlátozzák. Ezek a beállítások a tárhely szolgáltatóknál jellemzően be vannak kapcsolva.

Safe mode

A nyelv - különösen a PHP 3 megjelenése után - rendkívül gyorsan elterjedt. A dinamikus weboldalak kiszolgálására alkalmas szolgáltatásokat nyújtó cégek tömege jelent meg. Mindegyikük azt használta ki, hogy egy-egy ember vagy kisvállalat weboldalának terheltsége minimális, így egy szerver ki tud szolgálni sok megrendelőt. Ez a koncepció azonban egy hatalmas sebezhetőséget hordoz magában.
Az ilyen webhosting cégek a következőképpen működtek: minden felhasználó kapott egy FTP-vel elérhető tárhelyet, melyen természetesen csak a saját fájljait érte el a szerveren. Ide lehetett feltölteni a saját weboldal kiszolgálásához szükséges PHP szkripteket. A webszerverek már ekkor is támogatták az úgynevezett virtual host szolgáltatást. A virtual host használatával a szerver különbözőképpen reagál, ha különböző domain neveken keresztül érik el. Például legyen egy hosting cég szervere az 1.2.3.4-es ip címen, erre mutat egyébként a www.laciweb.hu domain is. A LaciWeb 2000 Bt. webhosting szolgáltatásokat nyújt, két felhasználója pityu és jozsi néven regisztrál. Ők a tulajdonosai a pityu.hu és a jozsi.hu domain-eknek, és a szerződés megkötésekor a domain-eket átirányítják az 1.2.3.4-es ip címre. Tehát mindkét (a www.laciweb.hu oldallal együtt mindhárom) domain-re érkező kérést ugyanannak a szervernek kell kiszolgálnia. HTTP/1.1 verziótól a protokoll elküldi a kért domain-nevet is, így a kérés feldolgozásakor a webszerver meg tudja különböztetni, hogy melyik oldalt szeretné a böngészgető személy megnézni, és ez alapján pityu vagy jozsi - vagy a cég - mappáját használja a weboldal kiszolgálására.

Látszólag semmi gond nincs az elgondolással, és így sok ember személyes weboldala kiszolgálható egy szerverrel, így a költségek minimálisak. Ha viszont belegondolunk, hogy a webszerver hogy szolgálja ki a kéréseket, akkor felbukkan a sebezhetőség: Mivel egy webszerver folyamat fut, és ez a kérések alapján szolgálja ki egyszer az egyik, egyszer a másik oldal kéréseit, és ennek a programnak hozzá kell férnie minden felhasználó PHP szkriptjeihez. Ez rendben is lenne, ha a reláció fordítva nem állna fenn: minden PHP script hozzáfér a többi PHP scripthez. A teendő csak annyi, hogy a basedir(__FILE__) paranccsal megállapítjuk a jelenlegi mappát, ez valószínűleg egy olyan mappa, melynek szülője tartalmazza az összes előfizető mappáját (pl.: /var/www/jozsi). Ezek alapján elég valószínű, hogy pityu mappája a /var/www/pityu lesz. Józsinak mindössze annyi dolga van, hogy az fopen vagy file_get_contents parancsok valamelyikével megnyissa (vagy akár át is írja) pityu fájljait, akár az adatbázis kapcsolódáshoz szükséges jelszót is kiolvashatja belőle. Ezt a módszert hívják Directory Traversal-nak.
Ezen probléma orvoslására a PHP fejlesztők megalkották a safe_mode beállítást. Ez alapesetben ki van kapcsolva, de bekapcsolásakor letilt egy sor lehetőséget, mellyel a fenti és ahhoz hasonló problémák szüntethetők meg. Safe mode bekapcsolása esetén minden script csak olyan fájlokat érhet el, melynek tulajdonosa megegyezik (általában a webhosting szerverekre feltöltött fájlok tulajdonosa különböző, pl: pityu és jozsi, és a www-data csoportba tartoznak). Hasonló ellenőrzések zajlanak le többféle fájlműveletet végző parancsok esetén, például egyes adatbázis-kezelők használatakor is. A szerveren új folyamatok indítása esetén csak olyan program adható meg, mely a safe_mode_exec_dir beállítással megadott mappában találhatók, ezzel biztosítva, hogy a PHP által biztosított korlátozásokat ne lehessen megkerülni rendszerfolyamatok használatával. Egyéb korlátozások a http://www.php.net/manual/en/features.safe-mode.functions.php oldalon olvashatóak.

Bár a funkció által megoldott sebezhetőség igen komoly, a safe_mode beállítás a PHP 5.3-as verziójától kezdve deprecated, azaz még elérhető, de egy jövőbeni verziótól (nevezetesen PHP 6-tól) nem lesz használható, így a funkció használata nem ajánlott! A fejlesztők ezt a döntést azzal indokolták, hogy a safe_mode gyorssegély volt csupán a probléma megoldására, és a probléma PHP szintjén történő megoldása architekturálisan nem helyes, hanem operációs rendszer szintű beállításokkal kell szabályozni a hozzáféréseket. Jelenleg azonban nem érhetőek el operációs rendszer szintjén a probléma orvoslására alkalmas eszközök, így sok szolgáltató jelenleg is használja ezt a funkciót.

SQL Injection

Mivel a PHP szkriptek futása nem folyamatos, hanem oldalmegjelenítések esetén fut le (a PHP parancssorban való futtatásától vagy a grafikus felületű php alkalmazásoktól most tekintsünk el, ezek nagyon ritkák), így az adatok tárolására gyakran használatos olyan megoldás, mely a program futása után nem veszik el. Ilyen lehet például fájl, munkamenet (session) vagy adatbázis, de a fájlelérés-olvasás-írás nehézségei és a munkamenetek korlátai miatt leggyakrabban az adatbázist alkalmazzák, PHP esetén leggyakrabban pedig a MySQL adatbázis kezelőt, mivel a PHP-hoz hasonlóan ez is egyszerűsége miatt terjedt el.

Mint a nyelvben általában mindent, az adatbázisokat is könnyű elérni, és kezelni:

<?php
// Connecting, selecting database
$link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password') or die('Could not connect: ' . mysql_error());
echo 'Connected successfully';
mysql_select_db('my_database') or die('Could not select database');

// Performing SQL query
$table_name = 'my_table';
$query = 'SELECT * FROM ' . $table_name;
$result = mysql_query($query) or die('Query failed: ' . mysql_error());

// Printing results in HTML
echo "<table>\n";
while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
    echo "\t<tr>\n";
    foreach ($line as $col_value) {
        echo "\t\t<td>$col_value</td>\n";
    }
    echo "\t</tr>\n";
}
echo "</table>\n";

// Free resultset
mysql_free_result($result);

// Closing connection
mysql_close($link);
?>
A fenti szkript kiírja a my_table tartalmát egy HTML táblázatban, az oszlopok számához mindig igazodva. Látható, hogy nem kell Query/PreparedQuery objektumokat készíteni, és abba típushelyesen beilleszteni a változókat, mivel lehetőség van string-ek összekapcsolására, a PHP fejlesztők ezt ki is használták, és elterjedt megoldás lett, hogy így kezeljenek adatbázist.

Az ilyen és hasonló szkriptek írásakor a fejlesztőkben valószínűleg fel sem merült, mekkora hibát követtek el. A sebezhetőséget egy példán keresztül mutatom be:
Tegyük fel, hogy a User táblában tároljuk a felhasználónevet és a jelszót, ezek a mezők legyenek rendre username és password. Az egyszerűség kedvéért a jelszót plain-text formátumban tároljuk, azaz nem csinálunk vele semmit, mielőtt a táblába kerülne. Megjegyzés: Ilyen módon az állásáért egy kicsit is aggódó programozó nem tárol jelszót, bevett gyakorlat, hogy a jelszót valamilyen hash függvény használatával, például md5 használatával kódolják, és a jelszó ellenőrzésekor md5($password) értékét hasonlítják össze az adatbázis tartalmával. A hash függvények egyirányúsága miatt a jelszó nem visszafejthető, de a két hash összehasonlítása biztonságos megoldás. Amennyiben a szervert támadás érné, és az adatok idegen kezekbe kerülnek, a felhasználók jelszava nem tudódik ki, más oldalakon, ahol ugyanazt a jelszót használják, nem tudják a támadók használni.
Visszatérve az eredeti problémához; Tegyük fel, hogy a programozó az azonosítást az alábbi script segítségével végzi (az egyszerűség kedvéért most csak a query szerepel):

$query = mysql_query("SELECT * FROM User WHERE username = '$username' AND password = '$password'");

if (mysql_num_rows($query) == 1) { /* success */ } else { /* fail */ }
Első ránézésre a programmal semmi gond nincs, jozsi felhasználó és abc123 jelszó használata esetén lekérdezi az(oka)t a sor(oka)t, amelyben a mezők értéke megegyezik a megadottal. Ha van visszaadott sor, akkor az azonosítás sikeres, ha nincs, akkor sikertelen. Tegyük most fel, hogy az oldalra téved egy - a programozónál okosabb és gonoszabb - hacker (ha már az előző példában jozsi szúrt ki pityuval, a hackert hívjuk most pityunak).
Pityu megadja a bejelentkezése adatokat. Felhasználónév: jozsi. Jelszó ' OR '1' = '1. Ezzel természetesen nem juthat be, mert a jelszó nem egyezik (mármint jozsi ezt gondolta). A megadott adatokkal Pityu be tud jelentkezni az oldalra jozsi felhasználói neve alatt. Hogy történhetett ez, ha a jelszó nem egyezik? Értékeljük ki a mysql_query függvény belsejében lévő string-et:
$username = "jozsi";
$password = "' OR '1' = '1";
$str = "SELECT * FROM User WHERE username = '$username' AND password = '$password'";

echo $str;
// echoes: SELECT * FROM User WHERE username = 'jozsi' AND password = '' OR '1' = '1'
Így a jelszónak nem is kell egyeznie, az SQL kérés megváltozott, ha a adatbázis jelszó üres, vagy '1' egyenlő '1'-gyel, akkor az azonosítás sikeres (az utóbbi feltétel pedig nagy valószínűséggel igaz lesz).
Ez a sebezhetőség még a Directory Traversal-nál is súlyosabb, mivel ott hozzáférés kellett a célszerverhez, itt viszont elég, ha az oldal olyan adatot kér be, amelyet utána SQL kérésben használ. A probléma megoldására több lehetőség is fennáll. A legegyszerűbb az, ha a bemeneteket mindenhol ellenőrizzük, és csak betűket, számokat, stb. engedünk meg. Ez a megoldás azonban körülményes, és könnyen kimaradhat néhány ellenőrzés. Ezen kívül a többi megoldás a használt szövegek escape-elését végzi el. Az escape-elés az a folyamat, amikor egy string-et olyan formára alakítunk, hogy az az előző példához hasonló beillesztés esetén ne okozhasson problémát. MySQL esetén a mysql_real_escape_string függvény használandó (általánosan az addslashes függvény). Ez a "' OR '1' = '1" string-ből a "\' OR \'1\' = \'1" string-et állítja elő, így az nem tudja megbontani a $password változót körülvevő idézőjeleket.

Az előző bekezdésben említett escape-elés történhet kézzel, de ebben az esetben ugyanaz a probléma merül fel, mint a bemenet szűrése esetén. Az escape függvény elmaradhat, és máris sebezhető a kód. Egy másik megoldás lehet olyan adatbázis absztrakciós rétegek használata, melyek az escape-elést automatikusan megoldják. Ezek használatakor általában a query-t placeholder-ek használatával kell megírni (pl.: "SELECT * FROM User WHERE username = ? AND password = ?"), és a behelyettesítést a használt rendszer biztonságosan elvégzi.
Ilyen lehetőségeket nyújt például a MySQLi vagy a PDO könyvtár használata. Érdemes ezt az opciót választani, mivel extrém esetekben az escapelés maga is megkerülhetű a különböző unicode karakterkészletek használatával, míg ha az adatbáziskezelő tud arról, hogy mi paraméter és mi nem, akkor ilyen probléma nem fordulhat elő.

Manapság a PHP programozók nagyrésze valamilyen keretrendszert használ, mint például a Zend Framework, a Symphony, vagy a CakePHP. Ezek rendelkeznek a fentebb említett adatbázis absztrakcióval, Zend Framework-ben a WHERE feltétel például megadható az array( 'username' => $username, 'password' => $password ) tömbbel, és a select előállításához is csak a táblanevet kell megadni, így nincs lehetőség SQL Injection használatára a kód támadásához.

Az automatikus escape-elésre a PHP-ban bevezetésre került a Magic Quotes lehetőség. Ez részletesen a következő szekcióban kerül bemutatásra.

Magic Quotes

Magic Quotes használata során a külső forrásból származó adatok automatikusan escape-elésre kerülnek. A hatása ugyanaz, mintha minden külső forrásból származó string-re alkalmaznánk az addslashes függvényt. A Magic Quotes 3 beállítás használatával szabályozható.

Az első, és legfontosabb beállítási lehetőség a magic_quotes_gpc. Ez alapesetben be van kapcsolva. Ha a funkció engedélyezve van, akkor a három beviteli forráson - GET, POST, COOKIE (innen jön a gpc a parancs végén) - érkező adat escape-elésre kerül. A beállítás használatával biztosítható, hogy a felhasználó által küldött adattal SQL Injection típusú támadást lehessen végrehajtani.

A második a magic_quotes_runtime. Ez alapesetben ki van kapcsolva. Ha a funkció engedélyezve van, akkor az előző pontban felsorolt három forráson kívül egyéb külső forrásokból származó adat is escape-elve fog érkezni. Ilyenek lehetnek például az adatbázis kezelő, illetve a szövegfájl kezelő függvények által visszaadott értékek. Normál esetben az előző funkció engedélyezése elegendő az SQL Injection kivédéséhez, mivel csak ezen a három csatornán érkezik a felhasználóktól adat. Kivételt képez a fájlfeltöltés: ha sor kerül a feltöltött fájlok szövegszerű felhasználására, akkor szükség lehet ezen funkció bekapcsolására, de ilyenkor ajánlott inkább kézzel vizsgálni a bemenetet, illetve magát az adatbázis kezelést kialakítani úgy, hogy ne lehessen támadást intézni a weboldal ellen.

A harmadik lehetőség a magic_quotes_sybase. Ritkán használt beállítás, ha be van kapcsolva, akkor az aposztróf jelet escape-eli aposztróf jellel, és csak azt. Ha be van kapcsolva, akkor felülbírálja a magic_quotes_gpc beállítását.

A safe mode-hoz hasonlóan a magic quotes is deprecated-ként került megjelölésre a PHP 5.3.0 verziójától, és a 6-os verzióban már nem kerül bele a rendszerbe, így nem ajánlott a használata, illetve a programozás során nem ajánlott arra támaszkodni, hogy a magic quotes be lesz kapcsolva. Az SQL Injection kivédéséhez hasonlóan a modern PHP keretrendszerek biztosítnak valamilyen szintű támogatást a külső forrásból érkező adatok escape-elésére.

Cross-Site Scripting (XSS)

A modern webes alkalmazások nagy mértékben használnak JavaScript kódot, hogy a weboldal interaktív legyen, és a desktop alkalmazásokhoz hasonló élményt nyújtson (Web 2.0, Rich Web Application). Napjainkban JavaScript kóddal már szinte bármit meg lehet csinálni, nem csoda, hogy már vírus is létezik, melyet JavaScript-ben írtak.

A Cross-Site Scripting egy összetett támadási technika. Használatával főleg adatlopás lehetséges, de összetett szkriptek használatával tetszőleges művelet végrehajtható az áldozat által megnyitott weboldalon. A támadás elindításához a szerveroldali script sebezhetősége szükséges, de maga a támadás célja tetszőleges JavaScript kód futtatása az áldozat számítógépén. Mint az előző bekezdésben elhangzott, saját JavaScript kód bejuttatása esetén szinte bármilyen művelet végrehajtható, így például kattintások is szimulálhatóak. Ez azt jelenti, hogy ha az áldozat egy bank oldalán esik áldozatul XSS támadásnak, akkor a támadó egy bonyolult script segítségével a bejelentkezés után akár tranzakciókat is indíthat JavaScript segítségével.

A kód bejuttatása után a támadás már csak HTML manipuláció és adatgyűjtés, mely inkább hosszadalmas, mint bonyolult, a támadás elindítása nehezebb feladat, vagyis hogy célfelületet találjunk a támadás indításához.

JavaScript kód beillesztése nem bonyolult, mindössze egy

<script src="path/to/script.js" />
sor szükséges a weboldalon. Persze a weboldalt nem mi szolgáltatjuk, ezért nem kerülhet oda bármilyen információ, amit akarunk (idézet, Ismeretlen Szerző: Híres utolsó mondatok). Ha közvetlen nem is férünk hozzá a weboldal által küldött adatokhoz, a dinamikus weboldalak gyakran tartalmaznak olyan információt, ami a felhasználóktól származik. Gondoljunk csak egy keresőre. Egy mezőbe beírjuk a keresendő szót, a következő oldalon pedig nagy valószínűséggel a weboldal a következőhöz hasonlót ír ki: "Keresés a következőre: almafa".

Tegyük fel, hogy egy gonosz programozó - a változatosság kedvéért legyen most pityu - téved a weboldalra. Ő a keresőbe a következőt írja: almafa <script src="https://pityu.hu/gonosz/script.js" />
A következő oldalon a weboldal természetesen kiírja, hogy "Keresés a következőre: almafa <script src="https://pityu.hu/gonosz/script.js" />". Csakhogy ez nem ilyen formában fog megjelenni a weboldalon, a <script src="https://pityu.hu/gonosz/script.js" /> ugyanis egy értelmes HTML elem, mely feldolgozásra kerül. Ezzel a JavaScript kód futni kezd az adott számítógépen. Tehát már tudjuk, hogy pityu képes saját magától adatot lopni. Hogy lehet ezzel másokat támadni? Ők nem fognak ilyen kifejezéseket beírni a keresőmezőbe! (idézet, Ismeretlen Szerző: Híres utolsó mondatok). Ha ők nem is írnak be ilyen kifejezést, az adatok nagy része GET vagy POST csatornán keresztül jut el a szerverhez, ilyen üzenetet küldeni pedig egyszerű.

Példaként a www.hwsw.hu weboldal keresője a http://hwsw.hu/kereses/almafa linket használja a keresések lebonyolításához, és a keresett kifejezést ki is írja a weboldalra, igaz, ez az oldal szűri a bemenetet, és kiveszi belőle a html elemeket, így nem támadható XSS technikával. Tekintsünk el most ettől, és tegyük fel, hogy az oldal támadható. A Támadónak mindössze annyi a dolga, hogy rávegye az áldozatát, hogy megnyissa a http://hwsw.hu/kereses/almafa<script%20src="http://pityu.hu/gonosz/script.js"> linket, és az oldalon máris elindult script.

A támadás kivédésére a legjobb módszer most is a bemenet szűrést vagy escape-elése, melyhez - mint ahogy az előző fejezetekben elhangzott - a modern keretrendszerek nyújtanak támogatást. Az XSS technikáról bővebben a http://en.wikipedia.org/wiki/Cross-site_scripting weboldalon lehet olvasni.

Bár az itt leírt példa kivitelezése körülményesnek tűnhet, a valóságban ennél jóval kifinomultabb módon is ki lehet használni az XSS technikát. Ha a támadónak sikerül egy banki oldalon ilyen sebezhetőséget találni, és azt kiaknázni, akkor egy spamlistára kiküldött hamis levelekkel rávehet felhasználókat arra, hogy a linkre kattintsanak, és így a támadás áldozatává váljanak.

Ennek a technikának alapvetően kétféle fő változatát szokták megkülönböztetni, az egyik az úgynevezett tárolt a másik pedig az átirányításos megoldás.
A korábbi keresős példa esetében az injektált script nem kerül fixen az oldal tartalmába, hanem az áldozatot magát kell rávenni arra, hogy egy megfelelően hamisított linkre kattintson ezáltal ő maga futtassa le a scriptet a saját böngészőjében.
Tárolt esetről akkor beszélhetünk, ha maga az oldal biztosít valamilyen lehetőséget arra, hogy a kódunkat fixen a forráskódba tudjuk helyezni és ezáltal az minden, sima megnyitásra is lefut.
Például egy vendégkönyvben ha nem megfelelően szűrik a bejegyzések tartalmát, akkor lehetséges kártékony kódot csatolna az oldalhoz, ami minden alkalommal a vendégkönyv bejegyzés listázó oldala megnyitásakor lefut a látogatók gépén.
Ez az eset sokkal veszélyesebb, mivel nem szükséges hozzá SPAM-eléssel rávenni a felhasználókat arra, hogy rányomjanak egy gyanús linkre.

File inclusion

Alapvetően kétféle file inclusion sérülékenységről beszélhetünk.
Local file inclusion esetén a helyi gépen férhetünk hozzá fájlokhoz, míg remote file inclusion esetén egy távoli helyről is be tudunk hívni kódot az oldalba.
Ez utóbbi formája a veszélyesebb, mivel könnyedén elhelyezhetünk egy egyszerű szöveges állományban bármilyen PHP kódot, aminek a futtatására rá tudjuk venni a cél rendszert.

if (isset( $_GET['COLOR'] ) ){
include( $_GET['COLOR'] . '.php' );
}

A fenti példában egy tipikus file inclusion hibát láthatunk, mivel mindenféle szűrés nélkül csak átadjuk a kapott paraméterünket az include függvénynek.
Amennyiben a szerver engedélyezve van az allow_url_fopen direktíva, máris megadhatunk akár URL címeket is a color paraméterben.
Pár példa, hogy a támadó miként tudná kihasználni a helyzetet:

Védekezni az ilyen támadások ellen úgy lehet a legkönnyebben, ha egyáltalán nem teszünk input adatokat az include vagy reqire függvényekbe. Vagy ha mégis, akkor szűrjük a bemenetet.
Ezek mellett pedig a scriptek a webszervert futtató felhasználó jogosultságával fognak futni (Debian, Ubuntu és hasonló linux szerverek esetén ez a www-data), így célszerű a jogait kordában tartani, vagy akár a chroot-al egy megadott területre bezárni.