Bezpečnost a expirace sessions v PHP

(publikováno 09.05.2017) 7 PHP, Bezpečnost, Windows, Linux

Techniky, jak nastavit session v PHP aby se zabránilo případným útokům. Ať už pomocí ini_set nebo vlastního řešení a zdůvodnění, proč expirace session někdy zabere dny a jak tuto nepříjemnost vyřešit.

Bezpečnost a expirace sessions v PHP

Tento článek patří do seriálu Security headers. Ostatní články seriálu:

  1. Security headers - Co jsou a jak na ně
  2. HTTPS nestačí, jak na HSTS a HPKP
  3. Bezpečnost a expirace sessions v PHP
  4. Content Security Policy
  5. Report URI - Správce reportů z prohlížeče

Bezpečnost je velmi důležitá a v poslední době čím dál více. Přesto v základním nastavení jsou session v PHP náchylné k různým útokům. Kromě jiného, platnost session může být mnohonásobně delší, než je nastavena.

UPDATE 30.8.2018 - Oprava podmínek viz komentář od Richarda - Díky

Nastavení chování session

Veškeré nastavení chování session je nastaveno v php.ini. To lze naštěstí na většině hostingů přepsat a je to dobře. Zde je seznam, co je dobré nastavit pro lepší zabezpečení.

// ==== Kód provést před zavoláním session_start() ====
// Nastaví název cookie, pouze kosmetická úprava
ini_set("session.name", "BlogSid");
// Cesta v cookie, na kterých adresách je viditelná
ini_set("session.cookie_path", "/");
// Cookie není možné přečíst v JavaSciprtu
ini_set("session.cookie_httponly", true);
// Session ID akceptuje pouze v cookies, nikoli v URL
ini_set("session.use_only_cookies", true);
// Při obdržení SID, které neexistuje, je vygenerováno nové
ini_set("session.use_strict_mode", true);
// Zákázání transparentního SID
ini_set("session.use_trans_sid", false); 
// Cookie se SID je odeslána pouze po zabezpečeném připojení
ini_set("session.cookie_secure", true);
// Místo ukládání session souborů
ini_set("session.save_path", "path/to/folder");

Popis nastavení

Většina nastavení je zřejmá. Vlastnost session.name pouze změní název cookie z defaultního PHPSESSID, nijak nezvýší bezpečnost. Pokud web běží na adrese example.com/mujweb/ je dobré nastavit cookie_path na /mujweb/, protože stránky, které nezačínají touto cestou, session cookie neuvidí. Pokud web běží na HTTPS, lze nastavit u cookie, aby ji prohlížeč neodesílal po nezabezpečeném připojení. To lze pomocí vlastnosti session.cookie_secure.

Důležité je nastavit session.cookie_httponly na true. Tímto lze zabránit krádeži SessionID (SID) pomocí XSS, protože škodlivý JavaScriptový kód nemá k této cookie přístup. Další důležitou vlastností k nastavení je session.use_only_cookies, session.use_trans_sid a session.use_strict_mode, která zabezpečuje proti útoku Session fixation.

Defaultní hodnoty session.save_path je složka /tmp. Pokud tato složka je sdílená mezi všemi weby na hostingu, lze takto jednoduše získat SID a případná data v $_SESSION. Přepsáním hodnoty lze přesunout soubory session někam na bezpečnější místo.

Session fixation attack a ochrana

Útok spočívá v odeslání uživateli odkazu na stránku, která přijímá SID také pomocí GET/POST metody. Server poté na základě tohoto SID vytvoří session a útočník má přístup, protože ví, jaké SID má oběť. Při nastavení session.use_only_cookies na true budou hodnoty SID v GET/POST parametrech ignorovány. Detailněji je popsán útok na Wikipedii.

Programová ochrana proti session fixation

Pokud se přeci jen útočníkovi povede dostat do prohlížeče cookie se SID, například pomocí XSS, výše zmíněné nastavení nepomůže. Je možné trochu ochranu zvýšit ještě v kódu. Po každém přihlášení a poté i v určitých časových intervalech přegenerovat SID pomocí funkce session_regenerate_id(). Pokud útočník SID podstrčí a uživatel se přihlásí, bude mu vygenerováno SID nové a útočníkovi staré SID k ničemu.

$sessionRegenerateID = 600; // 10 minut
if (!isset($_SESSION['regenerate']) ||
        $_SESSION['regenerate'] + $sessionRegenerateID < time()) {
    session_regenerate_id(true); // true -> vymaže starou session
    $_SESSION['regenerate'] = time();
}

Expirace session

Někdy se stane, že je uživatel odhlášen po půl hodině neaktivity, někdy až po 2 dnech. Způsobeno je to chováním garbage collectoru v session. PHP přečte soubor a naplní proměnnou $_SESSION. Ten již ale může být zastaralý, existuje ale proto, že je GC doposud nesmazal. GC se totiž spouští při každém zavolání session_start() pouze s pravděpodobností session.gc_probabilitysession.gc_divisor, ta je defaultně 1/100.

Problémy s file systémem FAT

PHP určuje jestli je soubor zastaralý pomocí vlastnosti atime (access time), neboli kdy bylo k souboru naposled přistoupeno. File systém FAT ale tuto vlastnost nemá a využívá se vlastnost mtime (modified time). Pokud se ale do $_SESSION nic nezapíše, soubor se nepřepíše a může být smazán mnohem dříve. Lze tedy do proměnné ukládat aktuální čas k přinucení přepsání souboru, nebo nastavit session.lazy_write na false. 

Jak na vlastní expiraci

Pokud je žádoucí, aby byl uživatel vždy odhlášen po určité době, nelze se pouze na session spoléhat. Lze to lehce vyřešit programově a zapisovat si poslední čas aktivity a pokud je moc starý, vymazat obsah $_SESSION. Zde se nedoporučuje používat session_destroy().

$sessionLifetime = 1800; // 30 minut
ini_set("session.gc_maxlifetime", $sessionLifetime);
if (!isset($_SESSION['atime']) ||
        $_SESSION['atime'] + $sessionLifetime < time()){
    $_SESSION = ['atime' => time()]; // Vymazat vše a nastavit čas
}else{
    $_SESSION['atime'] = time();
}

Máte zkušenosti s krádeží SessionID, nebo jsem zapomněl na něco důležité? Podělte se v komentářích. Úvodní obrázek je z Freepik a FlatIcon.

K tomuto článku již není možné přidávat další komentáře

Komentáře

Díky za super články k bezpečnosti ;)

Ahoj, neměla by se proměnná s délkou času k expiraci/regeneraci SESSION v podmínkách přičítat k uloženému času a to porovnat s aktuálním časem? Takhle se to odčítá a pokaždé bude ta podmínka pravdivá.

Dobrý postřeh. Samozřejmě máš pravdu, musí se to přičítat. Díky za komentář, chybu opravím ;-)

Ahoj, ještě jsem zapomněl, že když děláš vlastní expiraci, tak musíš po smazání všeho hned uložit do session atime, jinak tam ta proměnná nebude a tím pádem se pokaždé vymaže.

Díky za upozornění. Tuto techniku jsem osobně nikde nepoužil a neotestoval pořádně. Vidím, že příště bych neměl psát kód naslepo, ale i vyzkoušet.

Nebylo by vhodnejsi pro ochranu proti XSS, generovat (session_regenerate_id) pri kazdem nacteni stranky? Kdyz nekdo podstrci SID, tak ma (dle sessionRegenerateID=10 minut) spoustu casu, aby provedl nechtene operace v systemu. Nevim, jak moc je vykonove zatezujici provest proces, pro zmenu SID pres regenerate_id.

Určitě ano, jenže jak píše dokumentace:

Warning - Current session_regenerate_id does not handle unstable network well. e.g. Mobile and WiFi network. Therefore, you may experience lost session by calling session_regenerate_id.

Takže volat to při každém requestu může zvýšit riziko odhlášení. Nehledě na výkonnost, se určitě taky projeví. Je možné, že problémů je ještě více, ale momentálně mě žádné další nenapadá