ugrás a tartalomhoz

PHP, valamennyire biztonságosabban

sunz · 2004. Május. 11. (K), 21.00
PHP, valamennyire biztonságosabban
Cikkemben néhány tippet, ötletet szeretnék adni a biztonság témájában. Ez a kérdés sajnos nem volt központi fontosságú, amikor a PHP még gyerekcipőben járt, ezért nem árt néhány olyan beállítással illetve technikával megismerkedni, amelyek ezeket a - részben máig megmaradt - problémákat segítenek megoldani. Az operációs rendszer jogosultságainak megfelelő beállításával és az Apache illetve a PHP néhány opciójának jó megválasztásával sok kellemetlenségtől megkímélhetjük magunkat. Írásom csak kisebb áttekintés az általam legfontosabbnak ítélt dolgokról, bővebb információt a PHP kézikönyv, és a megadott források nyújthatnak.

A cikkben az operációs rendszer specifikus részek Linuxra vonatkoznak, azon belül pedig Debianra, még konkrétabban a Potatora és Woodyra, a PHP specifikusak pedig Apache 1.3.x alatt futó PHP 4-re.

A biztonságot befolyásoló tényezőket két csoportra lehetne osztani. Egyrészt függhetnek a szerver beállításaitól, amelyek főleg a rendszergazdát érintik, másrészt függhetnek maguktól a futtatott PHP programoktól, amelyekért már a programozó a felelős. Valamennyire szétválasztható a kettő egymástól, de hiába ír a programozó majdnem tökéletes kódot, ha egyetlen kis hibát kihasználva hozzá lehet férni nem publikus tartalmakhoz is, mert nincs az Apache, illetve a PHP beállításai között korlátozva a fájlok elérhetősége. Ez persze fordítva is igaz, érdemes összhangban tartani a kettőt.

Az Apache beállításairól

Legalább a konfigurációs fájlokról vedd le a "world readable" jogokat. Felesleges bárkinek is tudnia ilyen forrásból a beállításokat tartalmazó állományok tartalmáról, naplózásról, virtuális hosztokról, egyebekről.

chmod -R go= /etc/apache/ /etc/php4/
A http.conf néhány opciója:

ServerTokens Minimal
Nem túl sűrűn használt. "Minimal"-on nem írja ki a használt modulokat, és azok verziószámát. Ez egyszerűen lekérdezhető: betelnetelsz az Apache portjára és a HEAD / HTTP 1.0\n\n begépelése után megkaphatod. Azon kívül, hogy megnézed, ellenőrizni tudod vele a használt modulok tényleges működését, illetve betöltődését. Nem túl sok haszna van, leginkább a támadókat illetve a webszerverekről statisztikát készítőket segíti. Alapértelmezésben pontosan megadja az Apache, PHP, és a többi éppen használt modul verziószámát, illetve az operációs rendszer típusát is. Az 1.3.12-es Apache-tól kezdve létezik egy újabb lehetséges értéke is, a Prod vagy ProductOnly, amelyek hatására csak egy sokatmondó Apache szót ad vissza a fejlécben.

Ha nem csak az alapértelmezett .php kiterjesztésű állományokban tárolod a programjaidat, függvényeidet, akkor ne feledkezz meg az

application/x-httpd-php phtml pht php
opcióba felvenni ezek kiterjesztéseit.

Egy require() vagy include() függvénnyel beszúrt config.inc fájlban tárolt, az adatbázishoz való kapcsolódáshoz szükséges adatokat (hoszt, felhasználónév, jelszó, stb.) az Apache nem fogja értelmezni PHP kódként, hanem egyszerűen elküldi a böngészőnek az állomány tartalmát, mindenféle feldolgozás nélkül.

Ha a támadónak sikerül kiderítenie az általad használt fájlok neveit, akkor egy újabb név/jelszó párossal lesz gazdagabb, hiszen egy mezei böngészőbe beírva az állománynevet, a szerver plain/text típusúként elküldi számára. Lehetséges megoldás az is, hogy az így betöltött fájlokat a weblap gyökérkönyvtárán kívül tárolod, mely esetben az Apache nem fogja ezeket kiszolgálni.

A php_value, php_flag, php_admin_value és php_admin_flag

Ezek segítségével az Apache konfigurációban lokálisan felülbírálhatjuk a php.ini-ben szereplő beállításokat, külön könyvtárakra, virtuális hosztokra vonatkozóan. Az 'admin' beállítások véglegesített értékeket jelentenek, azaz ezeket nem lehet később módosítani (.htaccess állományban, vagy a httpd.conf további részében).

A fő php.ini-ben érdemes a legbiztonságosabb, legszigorúbb beállításokat meghagyni, és a programozói, tesztelői könyvtárak, virtuális hosztok számára a fejlesztés meggyorsítása végett enyhíteni a biztonsági beállításokon, engedélyezni a kliensnek kiküldhető hibaüzeneteket, egyéb kényelmi szolgáltatásokat, és erősen leszűkíteni azoknak az IP címeknek a számát, amelyekről elérhetőek a fejlesztés alatt álló programok. Természetesen egyáltalán nem ajánlott élesben futó szerveren tesztelni.

A már élesben futó webhelyek hibaüzeneteit érdemes naplózni, és időnként belenézni, javítani az esetleg újonnan kiderült hibákat.

A php_value-vel és a php_flag-gel a .htaccess fájlokban (ha ezek engedélyezve vannak) felül lehet bírálni a még nem véglegesített beállításokat. Ezzel eléggé le lehet húzni az alapértelmezett nagyfokú védelmet, figyeljünk oda, hogy kinek engedélyezzük a .htaccess fájlok használatát (amit akár egy hibás kód miatt egy támadó is átírhat), ilyen helyeken véglegesítsünk minden fontosabb direktívát az Apache konfigban.

Sajnos ezeket a beállításokat az ingyenes szolgáltatók egy része sem alkalmazza, pedig néhány alább ismertetett opció segítségével megvédhetők lennének a felhasználók fájljai, könyvtárai egy másik, rosszindulatú felhasználótól. Biztonságos lenne ingyenes szerveren olyan programokat futtatni, amelyek szöveges állományokban tárolnak adatokat, adatbázis szerver hiányában.

A php.ini fontosabb opciói, kapcsolói


register_globals = Off
A klienstől különböző formában (GET, POST, cookie, stb.) érkező adatokat nem illeszti be a globális változók közé, hanem különböző tömbökön ($_SERVER, $_GET, $_POST, stb) keresztül lehet elérni őket, így nem tudják felülírni az esetleg a kódban felejtett, kezdőérték nélküli változókat. Érdemes rászokni ezeknek a használatára, hiszen ha mégis be van kapcsolva a register_globals, akkor is elérhetőek ezek a tömbök. Továbbá ez a módszer garantálja, hogy a változóidat tényleg onnan kaptad, ahonnan várod őket. Ha minden űrlapnál GET-tel küldöd el a mezőket, akkor a $_POST tömbben megjelenő változók nagy valószínűséggel valamiféle amatőr támadási próbálkozás jelei, érdemes lehet naplózni. A $_* tömbök csak a PHP újabb verzióiban érhetőek el, a régebbiekben használhatóak a $_TIPUS = $HTTP_TIPUS_VARS értékadások a program elején, melyek esetleg az auto_prepend_file opcióval automatikussá is tehetők. Ha véletlenül egy Off értékkel "megáldott" szerveren kell futtatni a nem ilyen szemlélettel írt programokat, akkor sajnos nagyon nagy munka (lenne) átírni őket, ezért ilyen esetekben nem biztos, hogy megéri.

display_errors = Off
Szüksége van arra a látogatóknak, hogy lássák a hibás PHP programok, nem elérhető adatbázis szerverek, stb. által generált hibaüzeneteket? Szerintem nincs, csak a támadókat segítik az ilyen módon kiadott információk, a becsületes látogatók számára ezek semmit sem fognak jelenteni. Éles webszerveren öngyilkosság bekapcsolni.

log_errors = On
Egyértelműen be legyen kapcsolva. Naplózzunk egy állományba, melyet aztán a tail -f logfile paranccsal lehet valós időben figyelni. Segítségével olyan hibaüzenetek is felfedezhetünk, amit esetleg a böngésző elrejtett volna előlünk, vagy a html forrásban véletlenül például a <!-- --> tagek közé került volna.

magic_quotes_gpc = Off
Az addslashes() függvény használata nélkül SQL függvényekhez történő hozzáférés biztonságosságán javít, magyarul a vezérlő karakterek elé rak egy \ jelet, ezzel megvédi az adatbázist az SQL parancsba valamiféle módon bekerülő kártékony kódoktól, amit az adatbázis szerver valószínűleg parancsként értelmezne. Rontja a teljesítményt, növeli a programozói hanyagságot, és nem minden adatbázisszerverrel működik együtt. Ráadásul gondot okoz akkor, ha nem rögtön adatbázisba tesszük az adatokat, hanem újra megjelenítjük mondjuk egy űrlapban. Ezért javasolt az adatbázisnak megfelelő escape megoldás használata: pg_escape_string(), mysql_escape_string(), sqlite_escape_string(), stb.

error_reporting = E_ALL
Logolj minden hibaüzenetet!. Az alapbeállítás majdnem ugyanez, csak a megjegyzések nélkül, amik hasznosak szoktak lenni az alapos programozók számára (változó nem beállított kezdeti értékkel, stb.).

safe_mode = On
Letiltja, korlátozza egy nagyobb adag függvény használatát és bevezet néhány biztonsági ellenőrzést az állományokkal kapcsolatban. Érdemes használni, főleg ha nem a saját programjaidat futtatod.

safe_mode_gid = Off
A safe_mode csak azokat az állományokat engedi megnyitni, amelyek felhasználó azonosítója megegyezik a PHP szkript UID-jével. Ha ezt is bekapcsolod, akkor a csoport azonosítót is ellenőrzi.

open_basedir = /var/www/
Segítségével Korlátozhatod az állomány-, és könyvtár kezelő függvényeket, ha be van állítva, akkor az itt megadott könyvtárakon kívül nem nyithat meg a PHP egyetlen fájlt, könyvtárat sem. A záró / fontos, mert ezt hasonlítja össze az értelmező az állomány abszolút nevével, és ha egyezik az eleje, akkor engedélyezi a megnyitást. A /var/www esetén ha létezik például egy /var/www2 könyvtár, akkor az is megnyitható.

disable_functions =
Elérhetetlenné teszi az itt felsorolt függvényeket, például a phpinfo(), az exec(), a mail(), stb. függvényekre érdemes használni.

max_execution_time = 30
Maximum ennyi ideig (másodperc) futhat egy szkript, de ez csak a PHP kódokra vonatkozik, egy bonyolultabb SQL lekérdezéssel 500 másodperc felé is sikerült már tornázni 30-as alapbeállítás mellett.

allow_url_fopen = Off 
Felesleges, ha nem használjuk, kapcsoljuk ki, különben akár proxynak is használhatják a szervert, letöltve más tartalmakat.

error_log = syslog
A hibaüzenetek helye, melynek állománynév is megadható. Ha nincs beállítva, akkor az alapértelmezett ErrorLog-ba (ez Apache opció) mennek a hibaüzenetek.

enable_dl = Off
A PHP modulok dinamikus betöltése szkriptjeinkből. Safe mode esetén nem használható, más esetben is legyen kikapcsolva. Így elkerülhetjük, hogy olyan kiterjesztés kerüljön egy szkript alá, melyet nem akartunk engedélyezni.

file_uploads = Off
Ha egyik program sem tölt fel állományokat, akkor legyen Off, ha mégis akkor a php_admin_flag-gel engedélyezzük ott, ahol kell.

upload_tmp_dir =

Amíg nem kezelt a PHP egy feltöltött állományt, addig az ebben a könyvtárban tárolódik. Abban az esetben, ha az open_basedir be van állítva, akkor azon belül kell lennie, különben az állományt annak feltöltése után nem fogjuk tudni kezelni.

A hibaüzenetekről

Tesztelésnél, a program írásánál nagyon hasznosak, de csak a programozó számára, másokkal nem érdemes ezeket megosztani. A felhasználó felől érkező változóknál tapasztalt hibákat csak naplózni érdemes, a hiba okát, az adat ellenőrzésének részleteit lehetőleg ne küldjük ki a böngészőnek, syslog vagy egyéb naplóba írjuk. Ha a jelek mindenképpen arra utalnak, hogy valaki megpróbálja feltörni a programunkat, akkor küldjük át a klienst a főlapra, vagy egyéb módon szórakoztassuk a betörőt.

Kezdőérték nélküli változók

Ezek ellen tökéletes védelmet nyújt a register_globals opció kikapcsolása. Bekapcsolt állapotában az alábbi szkriptet elég könnyű lenne kijátszani, csupán csak a böngészőbe kellene beírni egy http://servercime/akarmi.php?auth=1 URL-t, és a jelszó ismerete nélkül hozzáférhetnénk a bizalmas adatokhoz. Ehhez persze szükséges az $auth változó nevének az ismerete, amit egy bekapcsolva hagyott display_errors opció vagy egy biztonsági mentés segítségével a támadó megtudhat.

<?php
 if ($pass == "hello") {
   $auth = TRUE;
 }
 ...

 if ($auth) {
   echo "szupertitkos információk";
 }
?>
A hiba megszüntethető az $auth változó kezdeti hamis értékének beállításával, illetve az első if feltételnél egy else ágban is pótolható a hiányosság.

Beérkező adatok ellenőrzése

Általános szabály az, hogy ne bízzunk meg egyetlen olyan adatban sem, ami a felhasználótól érkezik. A hidden típusú űrlap mezőn keresztül érkező adatokat se nehezebb variálni, mint a többit. A Javascript-es ellenőrzést csak a gyorsasága miatt érdemes használni, nem helyettesíti a PHP-n belüli ellenőrzéseket. Naplózzuk az olyan eseteket, amikor a beérkező változók nem normális, a programok megbuherálása nélkül előidézhetetlen kombinációjával találkozik a szkriptünk. Ha túl sokszor fordul elő a naplóban, akkor lehet, hogy mégis mi hibáztunk, és nem számtalan betörési kísérlettel van dolgunk.

is_* függvények

Arra valóak, hogy megmondják egy változó típusát. A böngészőtől érkező változók mindig 'string' típusúak, hiszen itt nem végez a PHP automatikus típusválasztást. Az is_numeric()-el lehet ellenőrizni, hogy egy karaktersorozat valóban számot tartalmaz-e.

A PHP a változók típusait automatikusan konvertálja, ha típuskonverzió nélkül kell összehasonlítanod két változót, akkor az === és társai nagy segítséget jelentenek, nem mindegy, hogy például egy SQL lekérdezés eredménye hibával tér vissza (FALSE), vagy nulla (0) rekorddal. Ha az ==-vel hasonlítod össze a lekérdezés eredményét tartalmazó változót, mindkét esetben ugyanazt fogod kapni.

htmlspecialchars()

Ha a felhasználótól érkező adatokat HTML kimenetbe ágyazva küldöd ki később a böngészőnek (pl. vendégkönyv, fórum), akkor elengedhetetlen a fenti függvény használata. Alkalmazása megvédi attól a weblapot, hogy a felhasználók HTML elemeket illesszenek be az adatok közé, melyek biztonsági kockázatok garmadáját nyitják meg.

Adatbázisfüggő escape megoldás

Ha az információk adatbázisban is tárolásra kerülnek, akkor erre is szükség lesz, különben akár egy véletlenül a szövegbe rakott aposztróf vagy idézőjel érvénytelenné teheti az SQL parancsot, illetve kárt is okozhat. Lásd fent.

.inc fájlok PHP-ben

A konfigurációs, illletve a függvényeket, inicializációs folyamatokat tartalmazó állományokat általában nem ajánlatos önmagában futtatni, ezért egy, a következőhöz hasonló ellenőrzéssel érdemes elküldeni a próbálkozó adatait a syslogba, vagy egyéb naplóállományba:

<?
// Közvetlenül ezt az állományt kérte a kliens
if (__FILE__ == $_SERVER["DOCUMENT_ROOT"] . $_SERVER["PHP_SELF"]) {
  // Naplózó kód kerül ide és kilépünk
  die();
}
?>

Amikor nincs szükség a PHP-re

Előfordulhatnak olyan esetek is, amikor a weblap olyan információkat (is) tartalmaz, amelyek ugyan dinamikusan változnak, de csak bizonyos időközönként. A pillanatnyi értékük meghatározott időintervallumonként mindig állandó (pl. az előző napi látogatók száma). Ilyenkor felesleges mindig végrehajtani a bonyolultabbnál bonyolultabb SQL lekérdezéseket, illetve egyéb programrészeket.

A crontab-ból periodikusan meghívott wget, ami fájlba menti az adott pillanatra jellemző PHP kimenetet, ideálisabb megoldás az ilyen esetekre, mint a lap minden lehívásakor legenerálni ugyanazt a végeredményt, és a processzort is kevésbé terheli. Amennyiben a weblap csak egyes részei tartalmaznak ilyen, bizonyos ideig állandó információkat, akkor az include() illetve a require() függvényekkel érdemes beszúrni a wget-tel lementett fájlt a megfelelő helyre.

Ezek a programok ne legyenek mindenki számára elérhetőek, mert akkor még véletlenül is kitalálhatja valaki az állomány nevét, és így szkriptünk kívülről jövő változókkal is futtathatóvá válik. Ha a gépen shellel rendelkező felhasználók is vannak, akkor csak localhost-ra korlátozni a szkript elérhetőségét sem jó megoldás, inkább a jelszavas elérés használatát javasolnám .htpasswd állományok alkalmazásával, amelyekre persze nem adunk mindenkinek olvasási jogot.

$HTTP_REFERER ellenőrzés

Ez a változó tartalmazza annak a (űr)lapnak a címét, ahonnan a látogató ide került. A böngésző állítja be, de nem minden esetben. Mivel ez is a felhasználótól jön, nem érdemes nagyon megbízni benne, még akár egy egyszerű telnet klienssel, vagy valamilyen nyílt forrású böngésző átírásával is könnyen lehet hamisítani. Ettől függetlenül érdemes lehet egy plusz védelmi vonalként használni, megvizsgálni, hogy az űrlapokon keresztül beérkező adatok tényleg a mi űrlapunk kitöltésével kerültek-e hozzánk.

$HTTP_POST_FILES illetve $_FILES

Az A Study In Scarlet - Exploiting Common Vulnerabilities in PHP című alapműben van egy elgondolkodtató példa a fájl feltöltés kihasználásáról, miszerint a file típusú beviteli mező által beállított globális változókat ($name, $name_size, $name_type, $name_name) akár GET metódussal is megadhatjuk a PHP-nek. A beviteli mező name tulajdonságának megfelelő változó tartalmazza a helyi fájl nevét, ahova a PHP ideiglenesen elmentette a feltöltött állományt. Ezt GET-tel beállítva mondjuk a /etc/passwd-re nagy valószínűséggel ezt a fájlt fogja olvasni a program. Az open_basedir helyes beállításával, illetve az is_uploaded_file() függvénnyel kiszűrhetőek az ilyen próbálkozások, de a legjobban itt is a registered_globals opció alapértelmezett bekapcsolt állapotának megváltoztatásával járunk. Ajánlom mindenkinek a fenti tömbök használatát, illetve a http://hu.php.net/manual/hu/features.file-upload.php címen található leírást.

Biztonsági mentések

Ha a szkripteket shellből is szerkeszted, akkor ügyelj a használt szerkesztők biztonsági mentéseire. Az akarmi.php szerkesztés előtti változatát könnyen elérheti bárki kívülről az akarmi.php.bak vagy akarmi.php~ néven (joe). Az nem megoldás, hogy a mentésekben használt kiterjesztésekre is engedélyezzük a PHP futtatását, mivel elképzelhető, hogy épp egy súlyos hibát javítottunk, de ott a régi, még hibás szkript is mindenki számára elérhetően. Ki kell kapcsolni a használt szerkesztők biztonsági mentési szolgálatását, vagy ezeket a kiterjesztéseket is elérhetetlenné kell tenni az Apache beállításával.

Sütik

Érzékeny adatok tárolására nem érdemes használni, egy webáruház esetén a vevők nagy mértékű kedvezményekben részesíthetik magukat, ha az árakat is a sütik tartalmazzák. Érdemesebb egy egyedi azonosítót generálni az időből és még valamilyen véletlen információból (IP cím, stb), majd megspékelni valamilyen hash függvénnyel, és mindezt egy SQL táblában tárolni, elsődleges kulcsként használva az így kapott azonosítót. A tábla többi attribútuma pedig logikusan a tárolandó adat legyen. Ha ez dinamikusan változik, akkor egy text típusú mezőbe (Postgresql esetén) elég sokmindent bele lehet zsúfolni egy tömb és a serialize(), illetve unserialize() függvény segítségével. Itt sem szabad elfelejteni a pg_escape_string() függvényt és testvéreit más adatbázisok esetén.

Példa egy virtuális hoszt beállításra:


<VirtualHost 192.168.1.254>
    ServerAdmin admin_email.cime
    DocumentRoot /var/www/vhosts/url.hu/htdocs
    ServerName url.hu
    ErrorLog /var/www/vhost/url.hu/logs/error_log
    TransferLog /var/www/vhost/url.hu/logs/access_log
    php_admin_flag safe_mode On
    php_admin_flag register_globals Off
    php_admin_flag magic_quotes_gpc Off
    php_admin_flag magic_quotes_runtime Off
    php_admin_flag magic_quotes_sybase Off
    php_admin_value doc_root "/var/www/vhosts/url.hu/htdocs/"
    php_admin_value open_basedir "/var/www/vhosts/url.hu/"
    php_admin_value upload_tmp_dir "/var/www/vhosts/url.hu/tmp/"
</VirtualHost>

Felhasznált irodalom

A Weblabor szerkesztői ajánlják továbbá a WFSZ biztonsági ajánlásainak áttanulmányozását is!
 
1

wget vs php4-cgi/php4-cli

Anonymous · 2005. Feb. 7. (H), 11.45
nehogymar lokalisan wget -en keresztul hivjal meg php-t, amikor parancssorbol is tudod futtatni...
2

Nem ugyanaz

Hojtsy Gábor · 2005. Feb. 7. (H), 13.12
Nem ugyanaz a PHP fut, ha szerveren keresztül kérsz le valamit, mintha parancssoros PHP-ből futtatod. Akár teljesen más verziók is lehetnek, de a konfigurációjuk is lehet más, még akkor is, ha ugyanaz a verzió. Egy környezetre ugye egyszerűbb fejleszteni meg hibát keresni is.
3

mindamellett

aries · 2005. Feb. 21. (H), 12.29
...mindamellett, hogy a cli változat megléte nem kötelező...
4

php cli vs cgi

Anonymous · 2006. Jan. 9. (H), 16.54
Ha mar ide tevedtem egy komment: de bizony erdemes :) mert pl egy service provider szeru felhasznalasnal esetleg eleg bonyolult a config (pl: chroot-olt apache, fastcgi-vel "kiadva" a php-re vonatkozo request-eket a chroot-on kivulre, ott custom setuid/setgid wrapper fcgi modban, onnan forkolt php interpreter a user home-jara chrootolva stb stb), szoval ha olyan igeny merul fel, hogy pl usernek legyen olyanja hogy valamit futtathat 1 orankent, akkor ahhoz a fenti eleg huzos configot ujra meg kene csinalni a CLI felhasznalas kerdeveert is. De ha mar mukodik igy sokkal egyszerubb pl orankent lenyomni egy wget-et a user pl cron.php-jara, es akkor pont a megfelelo modon fog mukodni anelkul hogy az egesz rendszer ujraimplementalnank csak ennek a kedveert ...