Nenechte si uhodnout Session ID
Při práci se sessions si mezi sebou server a klient neustále vyměňují SID. Jedná se o náhodně vygenerovaný token, podle kterého si server páruje dohromady jednotlivé požadavky konkrétního návštěvníka. Kdo zná SID, ten má přístup i k celé příslušné session.
Protože se na sessions obvykle váží takové citlivé věci, jako je přihlášení uživatele, stává se bezpečnost SID jedním z kritických míst aplikace. A jak ukážu v závěru článku, problém je obecnější a netýká se zdaleka jen sessions.
Uhodnout, spočítat, vymlátit
Útočník může získat token své oběti v podstatě třemi způsoby:
- spočítat či uhodnout její existující SID,
- odchytit její existující SID, například z probíhající komunikace či z logů (session hijacking),
- vygenerovat nové SID a oběti ho podstrčit (session fixation).
V tomto textu se zabývám první možností, zbylé dvě si zaslouží někdy v budoucnu každá přinejmenším jeden vlastní článek.
Základní pravidla pro tvorbu SID
Aby se minimalizovalo nebezpečí, že nám útočník naše SID zvenčí nějak spočítá, uhodne či odhalí pomocí útoku hrubou silou, je dobré při jeho generování respektovat následující principy. SID by mělo být:
- Unikátní
- Každá návštěva by měla dostat nový dosud nepoužitý token. Nemělo by se tedy stát, že se někomu přidělí již existující identifikátor.
- Náhodné
- Nemělo by být možné vyvolat či napodobit stejné okolnosti, které povedou k vygenerování stejného tokenu, jakou má již přidělenou oběť. Například pokud by se SID generovalo z IP adresy a User-Agenta, bylo by vytvoření vhodných okolností triviální.
- Nezávislé
- SID by nemělo být odvozeno od žádných smysluplných údajů, mělo by se jednat opravdu o náhodný identifikátor. V rámci tokenu by tedy neměly být žádné věci, jako je uživatelské jméno, IP adresa, aktuální timestamp, už vůbec ne citlivé informace, jako je například heslo. Token by neměl být od žádného podobného údaje ani nijak odvozen.
- Typickou chybou je například generování SID jako
md5(time()). Takto vytvoření identifikátor je odhadnutelný ve velice krátké době, protože je nutné pomocí útoku hrubou silou otestovat relativně málo možností. - Neodvoditelné
- Když si vezmu deset již existujících SID, neměl bych najít žádný systém, pomocí kterého dovedu určit jedenáctý platný token. Jinými slovy funkce, která SID generuje, by měla vykazovat velkou míru entropie.
- Překvapivě častý příklad chybného postupu je, že se tokeny generují jako lineární číselná řada. Příští platný token se pak dá snadno spočítat jako další číslo v řadě.
- Dostatečně dlouhé
- Útok hrubou silou je založen na tom, že útočník zkouší systematicky jeden identifikátor za druhým, dokud se netrefí. Čím je SID delší, tím více možností musí vyzkoušet. Bezpečný token má takovou délku, při které je tento způsob útoku v rozumném časovém horizontu výpočetně neuskutečnitelný.
- Expirující
- Při neomezené časové platnosti dávám útočníkovi možnost zkoušet brute-force attack neomezeně dlouho. Je tedy důležité platnost nepoužívaného tokenu časově omezit.
- Timeout se nejčastěji nastavuje na dobu někde mezi 5 a 30 minutami. V případě aktivní používané session je zase vhodné její SID průběžně obměňovat.
Jak tedy SID vygenerovat?
Standardní obsluha sessions, která je v PHP, uvedená pravidla vesměs zohledňuje. PHP aplikace, které používají standardní obsluhu sessions, jsou tedy samy od sebe relativně odolné, vše se děje transparentně a není u nich potřeba cokoliv řešit. Myslím ale, že i přesto je dobré si tyto na pozadí skryté vztahy uvědomit a ujasnit.
Někdy vás ale přeci jen může přepadnout potřeba si nějaký token generovat svépomocí. Základním pravidlem pak je vyhnout se psaní vlastních sofistikovaných algoritmů, které budou token nějak vymýšlet. Dost pravděpodobně totiž některý z výše uvedených principů porušíte a oslabíte tak bezpečnost celé aplikace.
Místo toho využijte klasický ověřený postup, kde se
pomocí uniqid() a rand() či lépe
mt_rand() vygeneruje pseudonáhodný řetězec. Pro případ, že
by ve stejný okamžik přišlo více různých dotazů, se navíc zkombinuje
s IP
adresou klienta. Nakonec se to celé prožene skrz nějakou hashovací
funkci:
$token = md5(uniqid(mt_rand()) . $_SERVER['REMOTE_ADDR']);
Ve starších verzích PHP nezapomeňte na začátku inicializovat generátor
pseudonáhodných čísel funkcí srand() respektive
mt_srand().
Více než jen sessions
Proč to ale celé píšu, když nám celou obsluhu sessions zajišťuje transparentně samo PHP? Nejde tu totiž zdaleka jen o sessions a jejich SID. Úplně stejné principy platí i pro jakékoliv další tokeny, které ve své aplikaci můžete využívat. Například:
- pro ověření nějaké akce jako ochrana proti CSRF,
- jako ověřovací klíč při e-mailovém potvrzování,
- pro zapamatování si aktuálního uživatele a zajištění permanentního přihlášení.
A ve všech těchto případech už si musíte nějaký ten identifikátor generovat a obhospodařovat sami.
Obzvláště poslední příklad představuje potenciální slabinu většiny dnešních aplikací, které permanentní přihlášení nabízejí, protože tento token už z principu nemůže expirovat a mimo jiným tak poskytuje i velký prostor právě pro brute-force attack. Pak musí nastoupit další ochranné mechanizmy, které možnost útoku hrubou silou omezí.
Na hrubý pytel hrubá záplata
Nejjednodušším opatřením je ještě prodloužit token, jehož otestování pak bude trvat o několik řádů déle. Obdobně můžeme cíleně prodlužovat prodlevy mezi jednotlivými odpověďmi na neúspěšné pokusy, čímž časová náročnost útoku opět rapidně vzrůstá. Také lze omezit počet unikátních tokenů, které se v daný okamžik uplatňují z jedné IP adresy.
Pokročilejší ochranu pak představuje detekce útoku hrubou silou a jeho následné odvracení. To se může dít v podobě obrany před DoS útokem třeba na úrovni firewallu či s pomocí modulu mod_dosevasive pro Apache, kdy lze přistoupit až k zablokování dané IP adresy, zahazování příchozích paketů apod.
Specialitkou jsou tokenové pasti. Jsou založené na myšlence, že si v sadě všech možných tokenů určíte některé, které nemohou být nikdy vygenerovány jako platné. Například řekněme ty, které začínají na nulu. Generovat nový token pak budete třeba takto:
do {
$token = md5(uniqid(mt_rand()) . $_SERVER['REMOTE_ADDR']);
} while ($token{0} == '0');
Tím zajistíte, že žádný z vygenerovaných tokenů na nulu začínat nebude. Důležité je, že zvolené pravidlo musí zůstat v tajnosti. Útočník, který bere jeden identifikátor za druhým, pak samozřejmě dříve či později zkusí otestovat i některý z neplatných tokenů. V okamžiku, kdy vám od uživatele přijde dotaz s tokenem začínajícím na nulu, hned víte, že se jedná o brute-force attack a můžete dotyčného bez milosti zablokovat.
Seriál o bezpečnosti sessions
- Nenechte si uhodnout Session ID (právě čtete)
- Session hijacking aneb ukradení Session ID
- Sidejacking aneb nasloucháme v síti
- Session ID do URL nepatří
- Předávání SID pomocí cookies
- Bráníme se zneužití ukradeného SID
- Session fixation aneb nenechte si podstrčit SID
