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.A 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
Ha nem csak az alapértelmezettopcióba felvenni ezek kiterjesztéseit.
Egy
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 szerverA
Ezek segítségével az Apache konfigurációban lokálisan felülbírálhatjuk a
A fő
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
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
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 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. Egyértelműen be legyen kapcsolva. Naplózzunk egy állományba, melyet aztán a Az 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.). 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. A 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ó Elérhetetlenné teszi az itt felsorolt függvényeket, például a 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. Felesleges, ha nem használjuk, kapcsoljuk ki, különben akár proxynak is használhatják a szervert, letöltve más tartalmakat. A hibaüzenetek helye, melynek állománynév is megadható. Ha nincs beállítva, akkor az alapértelmezett 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.Ha egyik program sem tölt fel állományokat, akkor legyen 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 A hiba megszüntethető az
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
A
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
■ 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/
http.conf
néhány opciója:
ServerTokens Minimal
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
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
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
log_errors = On
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
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
safe_mode = On
safe_mode_gid = Off
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/
/
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 =
phpinfo()
, az exec()
, a mail()
, stb. függvényekre érdemes használni.
max_execution_time = 30
allow_url_fopen = Off
error_log = syslog
ErrorLog
-ba (ez Apache opció) mennek a hibaüzenetek.
enable_dl = Off
file_uploads = Off
Off
, ha mégis akkor a php_admin_flag
-gel engedélyezzük ott, ahol kell.
upload_tmp_dir =
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 aregister_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";
}
?>
$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. Ahidden
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. Azis_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 afile
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. Azakarmi.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 egytext
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
- Secure Programming for Linux and Unix HOWTO
- A Study In Scarlet - Exploiting Common Vulnerabilities in PHP
- PHP kézikönyv
wget vs php4-cgi/php4-cli
Nem ugyanaz
mindamellett
php cli vs cgi