Miért szükséges védenünk munkameneteinket?
A munkamenet általában nem csak a felhasználók nyomon követésére
szolgál, hanem azok azonosítására is. Ezzel kapcsolatban két fogalmat
különböztetünk meg: authentikáció és authorizáció. Az authentikáció a
felhasználó azonosítását jelenti a rendszer számára, ez zajlik le,
amikor a felhasználó bejelentkezik rendszerünkbe. Ezt általában úgy
jelezzük, hogy a munkamenetben elhelyezünk egy változót, aminek
tartalma utal a felhasználó kilétére (például felhasználó azonosító).
Az authorizáció az a folyamat, melynek során ellenőrizzük, hogy
felhasználó számára egy funkció elérhető-e jogosultságai alapján. A
jogosultságok kiolvasása általában a bejelentkezéskor történik, és ezek
is a munkamenetben kerülnek tárolásra. Az authentikáció tehát egy
egyszeri cselekmény, míg az authorizáció (szükség esetén) minden egyes
oldalkéréskor végbe mehet.
Egy oldal lekérésekor a program megnézi, hogy a munkamenetben létezik-e
a látogató bejelentkezettségét jelző változó, ha igen akkor a
felhasználó személyazonosságát (és ezzel egyetemben a jogosultságait
is) ez alapján állapítja meg. Így ha a csúnya, gonosz támadónak sikerül
egy bejelentkezett felhasználó munkamenetét megszerezni, akkor ezzel
együtt megszerzi annak személyazonosságát is, az ő nevében lesz képes
használni a rendszert. Ehhez ennek a csúnya, gonosz (na jó, a
továbbiakban nem hangsúlyozom ki, de ne feledjük :) ) támadónak „csak”
annyit kell tennie, hogy valahogy megszerez egy éppen aktív
munkamenethez tartozó
sessionId-t.
Támadási módok
Alapvetően
kétféle módszer van. Az egyik a session hijacking (munkamenet
„eltérítés”). Ilyenkor a támadó egy már belépett felhasználó
sessionId-jét
próbálja meg megszerezni. A másik módszer a session fixation (munkament
„rögzítés”). Ennek lényege, hogy a támadó még a felhasználó
bejelentkezése előtt a megcélzott rendszeren létrehoz egy munkamenetet.
Ezt követően megpróbálja elérni, hogy az áldozat már a
bejelentkezésekor küldje el ennek a munkamenetnek a
sessionId-ját,
így elérve azt, hogy a bejelentkezését követően a rendszer ne hozzon
létre számára új munkamenetet. Így a támadó eleve tudja a használt
sessionId-t, bármikor képes átvenni az áldozat szerepét.
Session hijacking
Ezen módszer során háromféle lehetősége van a támadónak:
sessionId elfogása:
ez történhet például a hálózati forgalom figyelésével. Titkosított
kommunikáció használata kellő védelmet nyújt ellene, vagy ha erre nincs
lehetőségünk, akkor megtehetjük, hogy figyeljük a felhasználó IP címét,
böngészőjének általunk ismert paramétereit, melyek egy munkamenet alatt
biztosan nem változnak (tekintsünk el például ADSL kapcsolat esetén
történő napi kötelező IP váltástól), így ha ez mégis megtörténik, akkor
az valamiféle támadásra utal: megszüntetjük a munkamenetet. Ez a
megoldás korántsem nyújt akkora védelmet, mint a titkosított kapcsolat,
illetve alkalmatlan olyan támadási kísérlet kiszűrésére, amikor a
támadó IP címe, illetve az egyéb figyelt paraméterei megegyeznek az
áldozatéival.
sessionId megjóslása: az ez ellen való védekezés alapja, hogy minőségi, kellően kiszámíthatatlan
sessionId-t
generáljunk. Például gyakran látom kódokban, hogy ha egy hosszú
véletlenszerű karaktersorozatra van szüksége valakinek, akkor így
állítja elő:
md5(time()). Ennek hátránya, hogy egy jól felkészült támadó például egy md5 adatbázis segítségével hamar rájön, hogy a küldött
sessionId
számok md5-je, majd ezt követően az ütemezés alapján észreveheti, hogy
az másodpercenként változik, és ezen információk birtokában már jó
esélye van egy létező munkamenet
sessionId-jének kitalálására. Ezért célszerű a
sessionId
generálását véletlen számok alapján végezni, illetve mindig jól jöhet
valami egyéni „plusz” hozzáadása is. Alap beállítás szerint a PHP is az
aktuális időpont és egy véletlen szám alapján képzett md5 értéket
használ
sessionId-ként, de lehetőségünk van meg kicsit „turbózni” a
session.entropy_file és a
session.entropy_length opciók használatával.
brute force: a támadó próbálgatással igyekszik kitalálni a
sessionId-t. Az ilyen jellegű támadás ellen védelmet nyújt, ha a
sessionId kellően hosszú, tekintetbe véve az egyszerre párhuzamosan létező munkamenetek számát. Például ha a
sessionId
100 millió féle értéket vehet fel egyszerre, az elegendően soknak
tűnhet ahhoz, hogy a támadó véletlenül ráhibázzon egy éppen
bejelentkezett felhasználó munkamenet azonosítójára, de ha ebben a
rendszerben előfordulhat egyszerre mondjuk 1 millió bejelentkezett
felhasználó, akkor ez a szám már korántsem elegendően nagy ahhoz, hogy
a támadónak ne érje meg próbálkozással munkamenethez jutni.
A fentiekből láthatjuk, hogy egy már létező munkamenet megszerzése nem
könnyű feladat, ezért a második módszer eredményesebb lehet nem
megfelelő munkamenet kezelés mellett.
Session fixation
A támadás lényege, hogy a támadó megpróbálja rávenni a felhasználó böngészőjét, hogy az már a bejelentkezéskor küldjön egy
sessionId-t, így a rendszer nem fog számára újat kiosztani a bejelentkezés során.
A támadás három részre bontható:
- session setup (beállítás): a támadónak szereznie kell egy érvényes
sessionId-t
- session fixation: a támadónak rá kell vennie az áldozat böngészőjét, hogy majd használja ezt a
sessionId-t, amikor a felhasználó megpróbál bejelentkezni
- session entrance (belépés): a támadó várakozik, míg az áldozat belép, majd ezután használhatja annak munkamenetét.
Session setup
A szerver oldali nyelvek (webszerverek)
körében kétféle munkamenet kezelési mechanizmus létezik: „permissive”
(engedékeny) (ide tartozik a PHP is) vagy „strict” (szigorú). Szigorú
esetben a rendszer csak az általa korábban már ténylegesen kiosztott
sessionId-kat
fogadja el, ilyen rendszer esetén a támadónak létre kell hoznia egy
úgynevezett trap sessiont (csapda munkamenetet). Engedékeny esetben
tetszőleges
sessionId elfogadásra kerül, ilyen rendszereknél a támadó egyszerűen választ egy
sessionId-t.
Session fixation
Ha a
sessionId
átadása URL-en keresztül történik, akkor a támadónak rá kell venni az
áldozatot, hogy egy általa manipulált linken keresztül lépjen be.
Ha az átadás rejtett űrlap elemen keresztül történik, akkor a támadónak
rá kell venni az áldozatot, hogy egy általa készített űrlapon keresztül
lépjen be. Mindkét esetben „látszik” a manipulálás ténye (persze
előfordulhatnak
böngésző bugok is ;) ), ezért a támadások célja többnyire a sütin keresztül várt
sessionId-k. És mint előző cikkben említettem, ez (indokoltan) a
sessionId kedvelt tárolási formája.
A nehézséget az okozza, hogy a támadónak a betörés céljául választott
rendszer domainje nevében kell sütit elhelyeznie az áldozat gépén.
Lássuk milyen lehetőségei vannak:
XSS: a cél rendszer
XSS támadhatóságának
kihasználása egyéb, az áldozat által látogatott, a célrendszerrel egy
domainben lévő gép feltörése, majd a webszerver módosítása (amint az
áldozat oda látogat küld neki egy
sessionId sütit)
az áldozat DNS szerverének megtámadása: pl. az
akcio.otp.hu-hoz
rendelt IP cím a támadó gépére mutasson. Ezután küld egy levelet egy
erre az oldalra mutató linkkel, amire ha az áldozat rákattint, akkor az
elhelyezi a szükséges sütit.
hálózati forgalom figyelése, annak módosítása:
a támadó a hálózati forgalmat figyeli, vagy abba beépül (hogy rajta
keresztül menjenek az adatok), így manipulálni tudja a forgalmat, egy
sütit tud eljuttatni a felhasználó gépére.
Session entrance
Ha a
támadó sikeresnek bizonyult, akkor az áldozat belépését követően
egészen annak kilépéséig képes lehet az adott rendszerben a megtámadott
személy nevében ügyködni.
A munkamenet fixation elleni védekezés elemei:
- Minden esetben, amikor a felhasználó belép a rendszerünkbe, generáljunk számára egy új munkamenet azonosítót.
- A felhasználónak legyen lehetősége kilépni, mely mind az
aktuálisan, mind az esetlegesen korábban általa használt munkamenetek
megsemmisülésével jár. Ez ne csak a süti törlésével járjon, hanem
szerver oldalon is szüntessük meg a munkamenetet.
- Alkalmazzunk különböző időkorlátokat, melyek lejárta bizonyos
megszorításokkal járjon. Például 10 perc inaktivitás esetén a következő
oldallekéréskor kérjük be ismét a felhasználó jelszavát, 30 perc
inaktivitás esetén az ezt követően pedig törlődjön a munkamenet, a
felhasználónak újból be kelljen lépnie. Persze ezek a megszorítások
legyenek összhangban az oldal tartalmának megfelelő biztonsági
szinttel.
A keretrendszer részeként lesz még szó konkrét megvalósításról.
Biztonságos munkamenet kezelés
Láthattuk,
hogy alapvetően milyen fenyegetettségekkel kell szembenéznünk a
munkamenet kezelés során, illetve vázoltam, hogy milyen védekezési
lehetőségeink vannak ellenük. Lássunk most egy lehetséges
megvalósítást, mely igyekszik kellő védelmet nyújtani. Alapvetően a PHP
saját munkamenet kezelését használjuk, egy kis kiegészítéssel :-).
Biztonságos munkamenet kezelésre csak bejelentkezett felhasználók
esetén van szükség, hiszen különben érdektelen, hogy a munkamenetet
ellophatják-e vagy sem. Ha bármi miatt erre ellenkező esetben is
szükség lenne, minimális változtatások révén elérhetjük ezt.
A módszer lényege, hogy a bejelentkezett felhasználók munkameneteiről
adatokat tárolunk el adatbázisunkban, és ezen adatok alapján
ellenőrizzük, hogy egy kéréshez tartozó munkamenet érvényes-e, vagy
sem. A munkamenet a programunk elején automatikusan elindításra kerül (
session_start()),
majd az ellenőrzés kimenetelének függvényében, érintetlenül hagyjuk,
vagy töröljük. Az ellenőrzéshez használt adatbázis tábla szerkezete a
következő:
- CREATE TABLE "user_sessions" (
- ## azonosító
- "id" SERIAL,
- ## munkamenet azonosító
- "session_id" VARCHAR(32) NOT NULL,
- ## felhasználó azonosító
- "u_id" INTEGER NOT NULL,
- ## felhasználó IP címe
- "ip_address" VARCHAR(23) NOT NULL,
- ## felhasználó kliense által küldött user_agent
- "user_agent" VARCHAR(255),
- ## munkamenet létrehozásának időpontja
- "created" TIMESTAMP WITH TIME ZONE NOT NULL,
- ## legutolsó kérés időpontja
- "modified" TIMESTAMP WITH TIME ZONE NOT NULL,
- PRIMARY KEY("id"),
- UNIQUE("session_id"),
- UNIQUE("u_id")
- )
CREATE TABLE "user_sessions" (
## azonosító
"id" SERIAL,
## munkamenet azonosító
"session_id" VARCHAR(32) NOT NULL,
## felhasználó azonosító
"u_id" INTEGER NOT NULL,
## felhasználó IP címe
"ip_address" VARCHAR(23) NOT NULL,
## felhasználó kliense által küldött user_agent
"user_agent" VARCHAR(255),
## munkamenet létrehozásának időpontja
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
## legutolsó kérés időpontja
"modified" TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY("id"),
UNIQUE("session_id"),
UNIQUE("u_id")
)
A munkamenet ellenőrzését és az ezzel kapcsolatos teendőket a következő szerkezetű osztály valósítja meg:
- <?php
- define('SESSION_GLOBAL_TIMEOUT', 0);
- define('SESSION_REQUEST_TIMEOUT', 0);
- define('SESSION_INVALID_SESSIONID', -1);
- define('SESSION_NONEXISTENT_SESSION', -2);
- define('SESSION_GLOBAL_TIMEOUT_EXPIRED', -3);
- define('SESSION_REQUEST_TIMEOUT_EXPIRED', -4);
-
- class SessionManager {
- function SessionManager() {...}
- function _generateSessionId($userId, $startTime) {...}
- function _isWellFormedSessionId($sessionId, $userId, $startTime) {...}
- function destroySession() {...}
- function updateSession() {...}
- function validateSession() {...}
- function registerSession($userId) {...}
- }
- ?>
<?php
define('SESSION_GLOBAL_TIMEOUT', 0);
define('SESSION_REQUEST_TIMEOUT', 0);
define('SESSION_INVALID_SESSIONID', -1);
define('SESSION_NONEXISTENT_SESSION', -2);
define('SESSION_GLOBAL_TIMEOUT_EXPIRED', -3);
define('SESSION_REQUEST_TIMEOUT_EXPIRED', -4);
class SessionManager {
function SessionManager() {...}
function _generateSessionId($userId, $startTime) {...}
function _isWellFormedSessionId($sessionId, $userId, $startTime) {...}
function destroySession() {...}
function updateSession() {...}
function validateSession() {...}
function registerSession($userId) {...}
}
?>
A felhasználó bejelentkezését követően meghívjuk a
SessionManager objektum
registerSession metódusát átadva neki a felhasználó azonosítóját. Ez a következőket teszi:
- generál egy új munkamenet azonosítót, amelynek
egy része úgy képződik, hogy belekódoljuk a létrehozás idejét és egyéb
adatokat (IP, user_agent, stb.), amik alapján formai ellenőrzést tudunk
majd végezni a munkamenet azonosítón. A másik része egy véletlen
karaktersorozat. A két rész valamilyen általunk választott módszer
alapján való összekeveréséből jön létre a tényleges munkamenet
azonosító. Fontos, hogy ez az algoritmus két irányú legyen, hogy egy
kapott azonosítót és részeire tudjunk bontani.
töröl a munkamenet táblából minden korábbi olyan munkamenet bejegyzést,
amiben a munkamenet azonosító megegyezik a most generálttal, illetve a
felhasználó azonosító megegyezik az átadottal.
- ezután a munkamenet táblába bekerül egy bejegyzés, ami
tartalmazza a felhasználó azonosítóját, a munkamenet azonosítót és
minden egyéb szükséges adatot (IP cím, user_agent, létrehozás
időpontja, stb.).
- ezután újraindítja a munkamenetet a generált munkamenet
azonosítóval (ne felejtsük, hogy a munkamenet a programunk elején
automatikusan elindításra került). Itt figyeljünk arra, hogy a
$_SESSION
tömb tartalma elveszik, ezért ha tárolunk olyan adatot benne, amire még
szükségünk lehet (például melyik oldalról jött a felhasználó a
bejelentkező oldalra), akkor azt mentsük el.
- végül az újonnan létrejött
$_SESSION
tömbbe elhelyez egy változót, mely tárolja például a felhasználó
azonosítót, vagy a user_sessions tábla megfelelő sorának azonosítóját,
és amely jelzi, hogy az aktuális munkamenet egy bejelentkezett
felhasználóhoz tartozik.
Amikor egy kérés érkezik az alkalmazásunkhoz, akkor megnézzük, hogy
létezik-e a munkamenetünkben ez a változó. Ha igen akkor meghívjuk a
SessionManager objektum
validateSession metódusát, mely a következőket teszi:
- először egy formai ellenőrzést végez a
munkamenet azonosítóval kapcsolatban a belekódolt adatok és a kérés
ezeknek megfelelő adatai alapján.
- ha ez stimmel, akkor megnézi, hogy létezik-e a user_sessions táblában bejegyzés az adott munkamenet azonosítóval kapcsolatban.
- ha igen, akkor megnézi, nem járt-e le a munkamenet. Én két
időkorlátot definiáltam: a nagyobbik lejárta esetén nem tekintjük
érvényesnek a munkamenetet (töröljük azt, és a felhasználónak újból be
kell jelentkeznie a rendszerbe), míg a kisebbik lejárta esetén a
felhasználónak lehetősége van folytatni munkamenetét jelszavának
ismételt megadásával. A konkrét megvalósítás esetén célszerű úgy
eljárnunk, hogy mindkét időkorlát esetén nulla érték megadásával az
adott időkorlát figyelmen kívül maradjon.
- ha minden ellenőrzés sikeres volt, akkor frissíti a user_sessions tábla bejegyzés modified mezőjét.
A függvény hiba esetén visszaadja annak kódját, ellenkező esetben
true-val tér vissza. Jelenleg 4 féle hiba került definiálásra:
- a munkamenet azonosító formailag nem megfelelő;
- a munkamenet táblában nincs a munkamenet azonosítónak megfelelő bejegyzés;
- nagyobbik időkorlát lejárt;
- kisebbik időkorlát lejárt.
Az első 3 esetén meghívjuk a
SessionManager objektum
destroySession metódusát, mely törli az aktuális munkamenetet (esetlegesen meglévő
user_sessions
táblabeli bejegyzést), majd újraindítja a munkamenetet. Az utolsó hiba
esetén a felhasználót a bejelentkezési oldalunkra irányítjuk, ahol
jelszavának ismételt megadására szólítjuk fel.
Mit nyertünk?
Az általunk generált
sessionId,
ha jól csináltuk, akkor nehezebben megjósolható, mint a PHP által
generáltak, ráadásul formai megszorításokat is tartalmaz, így
ellenállóbb egy brute_force támadás ellen, hiszen a támadónak formailag
megfelelő azonosítókat kéne generálnia ahhoz, hogy eleve sikerrel
járjon. Szükség esetén számon tarthatjuk azokat az IP címeket, ahonnan
formailag nem megfelelő
sessionId-t tartalmazó kérések érkeznek, és ha ezek száma meghalad egy értéket, akkor tetszőleges megszorításokat eszközölhetünk ellenük.
Módszerünk alkalmazásával a PHP munkamenet kezelésének említett
permissive jellegét strict-é változtattuk, hiszen csak az általunk
nyilvántartott munkamenetek használhatóak.
A belépést követő, a munkamenet általunk generált azonosítóval történő
újraindítása kizárja a session fixation támadás lehetőségét, míg erre a
PHP saját munkamenet kezelése esetén lenne mód.
Bár a PHP is nyújt lehetőséget (
session.gc-maxlifetime)
a munkamenet maximális hosszának meghatározására, az általunk használt
időkorlátok bevezetése rugalmasabb kezelést tesz lehetővé. (Ezen kívül,
ha a fenti linkre kattintunk, olvashatunk róla, hogy a PHP 4.2.3-as
verziója előtt bizonyos fájlrendszerek esetén ez a beállítás nem
működött megfelelően.)
Még egy előnye ennek a módszernek, hogy teljesen független a munkamenet adatainak tárolási formájától. Így ha esetlegesen
saját kezelőt használnánk, mert például szeretnénk ezen adatokat adatbázisban tárolni, akkor ezt nyugodtan megtehetjük.
Pár plusz lehetőség
A
felhasználó kezelés gyakran párosul jogosultság kezeléssel is, az egyes
felhasználóknak eltérő jogosultságaik vannak. Amikor a felhasználó
bejelentkezik a rendszerbe, jogosultságai felolvasásra kerülnek az
adatbázisból, és általában a munkamenetben kerülnek tárolásra. Ezt
követően a jogosultságait nem frissítjük minden egyes oldallekéréskor
egy felesleges adatbázis műveletet végrehajtva. Ennek a módszernek
azonban egy hátulütője lehet, miszerint ha egy felhasználónak a
bejelentkezését követően megváltoznak a jogosultságai (az
adminisztrátor éppen ezen idő alatt változtatja őket), akkor a
frissítés nélkülözése miatt a munkamenetében és az adatbázisban tárolt
jogai nem lesznek konzisztensek, a felhasználó esetleg továbbra is
elérhet olyan funkciókat, melyekhez szükséges jogosultságokat már
megvonták tőle.
A probléma megoldására valahogy jeleznünk kell a változás tényét. Mivel a munkamenet ellenőrzése miatt a
user_sessions táblánk egy sorát úgyis lekérdezzük, célszerű lehet ezt itt jelezni. Elhelyezünk egy plusz mezőt, melyet
true értékre állítva jelezhetjük, hogy szükséges a jogosultságok újraolvasása. A
SessionManager
osztályunkat pedig bővítjük a mező értékének lekérdezését és állítását
lehetővé tevő metódusokkal. Ezután már csak annyi a teendőnk, hogy
amikor megváltoztatjuk a felhasználó jogosultságait, akkor frissítjük a
user_sessions táblát.
Még egy kényelmi funkciót érdemes lehet rendszerünkbe beépíteni. Tegyük
fel, hogy a felhasználó kitölt egy hosszú űrlapot, majd az elküldés
előtt valami tennivalója akad, és mire visszatér munkájához lejár a
tétlenséget definiáló kisebbik időkorlát. Mikor az űrlap elküldésekor
ezt érzékeljük és a bejelentkezési oldalra irányítjuk, ezek az adatok
elvesznek, a felhasználó jelszavának kitöltése után kénytelen újból
megadni őket. Ezen kellemetlen szituációk elkerülésére megtehetjük,
hogy az időkorlát lejárta esetén a kérés során küldött adatokat
lementjük munkamenetünkbe, és ha a felhasználó sikeresen megerősítette
azonosságát, visszatöltjük azokat, és az eredeti kérésnek megfelelően
folytatódik a program futása.
Néhány jótanács
Következzen
néhány jótanács a PHP munkamenet kezelésével kapcsolatban, melyek egy
részére a kézikönyvben is találunk utalást, de van ami személyes
tapasztalaton alapul.
Figyeljünk arra, hogy ha a
sessionId nem sütiben tárolódik, és egy
Location HTTP fejléccel szeretnénk a felhasználót átirányítani egy másik oldalra, akkor nekünk kell biztosítani a
sessionId elhelyezését az URL-ben.
Tartsuk szem előtt, hogy a $_SESSION tömb tartalmának előállítása fájl
műveleteken alapul, ezért érdemes minél kevesebb adatot tárolnunk
benne, nagy adatmennyiség használata esetén lassulást tapasztalhatunk.
Ha szükséges érdemes lehet egy ramdisken tárolni ezeket a fájlokat,
vagy saját kezelőt használnunk.
Ha objektumokat szeretnénk tárolni munkamenetünkben, akkor ezen osztályok definícióinak betöltése a
session_start() parancs előtt meg kell történjen. Ilyen esetben például nem használható a
session.auto_start lehetőség sem.
Figyeljünk arra, hogy a
$_SESSION tömb egy
super global tömb, ezért függvényen belül nem szükséges a
global kulcsszó használata, ellenkező esetben bár értékadásaink látszólag érvényre jutnak, nem kerülnek tárolásra.
És még egy dolog, ami már az előző cikkben szintén említésre került,
saját munkamenet kezelő használata esetén a programunk befejezésekor
hívjuk meg a
session_write_close()
függvényt, ezzel biztosan elkerüljük, hogy a munkamenet adatok még
azelőtt mentésre kerülnek, mire a következő oldal kiszolgálása során a
programunk újból használni szeretné azokat, ami az adatok inkonzisztens
állapotba való kerülését eredményezheti.