Hogyan tegyük biztonságosabbá a php -ban készült weboldalunkat!
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 php.ini fontosabb opciói, kapcsolói
A php.ini fájlt megtalálod a virtual szerveren a /conf mappában
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.
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
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.
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.
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
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.
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ó.
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.
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
ErrorLog-ba (ez Apache opció) mennek a hibaüzenetek.
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
Off, ha mégis akkor a
php_admin_flag-gel engedélyezzük ott, ahol kell.
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";
- }
- ?>
<?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:
- <?
-
- if (__FILE__ == $_SERVER["DOCUMENT_ROOT"] . $_SERVER["PHP_SELF"]) {
-
- die();
- }
- ?>
<?
// 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.