Süti-alapú bejelentkezés biztonságosabb használata

Gyakran használnak a weboldalon “Emlékezz rám” típusú, süti (cookie) alapú bejelentkezési módot. Ez tényleg nagyon hasznos szolgáltatás, ám legalább ekkora biztonsági rést is jelent, ha nem megfelelően kódoljuk le.

Hatalmas biztonsági kockázat, ha a felhasználói gépen – akár kódolva is – jelszavakat és felhasználóneveket (esetleg felhasználó azonosítót) tárolunk. Így ha bárki hozzájut ezekhez az adatokhoz (ami lássuk be, nem is annyira nehéz), máris tud küldeni egy hamis HTTP-kérést a fenti adatokkal, és be is van léptetve az oldalra.

A megoldás kézenfekvő: ne tároljunk felhasználóneveket és jelszavakat a felhasználók gépein. Azonban kell egy bármilyen adat, amivel egyértelműen tudjuk azonosítani a felhasználókat, és ami biztonságosan tárolható sütiben.

Tételezzük fel, hogy a felhasznalok táblában a következő adatokat tároljuk:

[A könnyebb érthetőség miatt a leírás során – szokásomtól eltérően – magyar neveket használok]

azon (Auto_inc-es int mező) felhasznalonev (Varchar(32)) jelszo (Varchar(32) vagy Varchar(40))

Biztonságosan egyiket sem tárolhatjuk a felhasználó gépén: vezessük be egy újat! Az új adatot egy külön táblában tároljuk, ez legyen például sutik.

azon felhasznaloAzon sutikulcs

Ezen táblánkban az azon szintén egy Auto inc-es egész szám mező, a felhasznaloAzon egy idegen kulcs (Foreign key), amely a felhasználó azonosítóját (felhasznalok.azon) tartalmazza, a sutikulcs pedig ismét csak egy 32 byte hosszúságú varchar mező [A brute-force-os megoldások nehezítésére], amelyet ráadásul egyedi kulcsként definiálunk (Unique).

Az eddigiek SQL-ben:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `felhasznalok` (
`azon` INT NOT NULL AUTO_INCREMENT ,
`felhasznalonev` VARCHAR( 32 ) NOT NULL ,
`jelszo` VARCHAR( 40 ) NOT NULL ,
PRIMARY KEY (`azon`), UNIQUE (`felhasznalonev`)
);
 
CREATE TABLE `sutik` (
`azon` INT NOT NULL AUTO_INCREMENT ,
`felhasznaloAzon` INT NOT NULL ,
`sutikulcs` VARCHAR( 32 ) NOT NULL ,
PRIMARY KEY ( `azon` ) , UNIQUE (`sutikulcs`)
);

A süti kulcsokat tárolhatnánk akár a felhasznalok táblában is, azonban egy-egy felhasználó több helyről is böngészheti oldalainkat, így azokat 1:n (egy felhasználóhoz több kulcs tartozhat) kapcsolattal egy új táblában tároljuk.

Akkor a következő lépés legyen a felhasználó-azonosításunk módszere. Ehhez először néhány dolgot jegyezzünk meg:

  • A süti-kódokat egyszer használatosak.
  • A süti-kódokat csak legvégső esetben ellenőrizzük, ha a munkamenet-alapú hitelesítés sikeres nem nézzük a süti tartalmát.
  • Bejelentkezéskor – akár az süti-alapú, akár a felhasználótól kértük az adatokat – új sütit állítunk be, ha kérte az azonnali beléptetést.

A következő kódnál feltételezem, hogy egy auth() nevű függvény vizsgálja az adatokat, a munkamenetben a ‘vendeg_id’, a ‘ip’ és ‘last_action’ elemeket tároljuk!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
/* A microtime függvény által visszaadott stringből float-ot készít */
function microtime_float() {
	list($sec, $usec) = explode(' ', microtime());
	return ((float)$sec + (float)$usec);
	}
 
/* TLoF auth függvénye */
function auth() {
	if(isset($_SESSION['vendeg_id']) and $_SESSION['ip'] == $_SERVER['REMOTE_ADDR'] and $_SESSION['last_action'] > (mktime()-600)) {
		$_SESSION['last_action'] = mktime();
		return true;
		}
	else {
		unset($_SESSION['vendeg_id']);
		$_SESSION['ip'] = '';
		$_SESSION['last_action'] = 0;
		return false;
		}
	}
 
/* Innentől jön a lényeg */
if(!auth()) { // Ha nincs bejelentkezve
	/* Itt ellenőrizhetjük a POST-ból érkező adatokat is, most az nem lényeges... */
	if(IsSet($_COOKIE['authData']) && strlen($_COOKIE['authData']) == 32) {
		$query = "SELECT `felhasznalok`.*, `sutik`.`azon` AS `sutiazon` FROM `sutik`
			LEFT JOIN `felhasznalok` ON `felhasznalok`.`azon` = `sutik`.`felhasznaloAzon`
			WHERE `sutikulcs`='" . mysql_escape_string($_COOKIE['authData']) . "'
			LIMIT 1"; // Ezzel a kéréssel azonnal a felhasznalok tábla megfelelő sorát kapjuk, plusz a süti DB-beli azonosítóját */
		$res = mysql_query($query); /* Lekérjük az adott sütikulcsot, ha van */
		if(Is_array(($row = mysql_fetch_assoc($res)))) {
			$_SESSION = array_merge($_SESSION,
				array('vendeg_id' => $row['azon'], 'last_action' => mktime(), 'ip' => $_SERVER['REMOTE_ADDR']));
			/* Frissítjük a sütikulcsot, és kiküldjük az új sütit:
				A sütikulcs a jelenlegi microtime md5 hashe.
				Ha már van ilyen (egyedi index a sutikulcs!) sor a táblában megpróbálunk újat generálni...
			*/
			$key = mysql_escape_string(md5(microtime_float()));
			$query = mysql_query("UPDATE `sutik` SET `sutikulcs`='" . $key . "' WHERE `azon`='" . $row['sutiazon'] . "'");
			$i = 0;
			while(!$query && $i <= 10) { // Maximum 10 próbálkozás, ha ezután is sikertelen hagyjuk ezt az egészet
				$key = mysql_escape_string(md5(microtime_float()));
				$i++;
				$query = mysql_query("UPDATE `sutik` SET `sutikulcs`='" . $key . "' WHERE `azon`='" . $row['sutiazon'] . "'");
				}
			if(!$query) { // Az utolsó is rossz volt
				setcookie('authData', '', time()-3600);
				}
			else { // Sikerült felvinni
				setcookie('authData', $key, time()+3600*24*30); /* Harminc napra állítjuk a sütit */
				}
			}
		else {
			/* Valami nem jó, töröljük a sütit... */
			setcookie('authData', '', time()-3600);
			}
		}
	}

Már a kommentekből is jól látszik a szkript működése, de azért mégegyszer (ha valamelyik lépés sikeres nem lép a következőre):

  • Megnézi, hogy a munkamenet (session) alapján azonosítható-e a felhasználó (auth függvény)
  • Ha nem, akkor ellenőrizheti a POST alapján (ezt most nem írtam, érdemes a süt ellenőrzés előttre betenni)
  • Ellenőrzi a sütiket, ha megfelelőnek találta (itt hossz alapján ellenőrizzük, hogy lehet-e md5 hash), akkor lekéri, hogy létezik-e ilyen sütiazonosító, ha igen, belépteti a felhasználót és megpróbál újat generálni.

    Itt a while szerkezetre azért volt szükség, mert a sutiazon mezőt egyedi kulcsként használjuk, tehát csak egyszer szerepelhet, így előfordulhat, hogy egyszer már felvitt hasht próbálunk használni, és ezredmásodpercre pontosan ugyanakkor történő bejelentkezéseket is lekezeltük.

Mint látható nem nagy munka egy valamivel biztonságosabb beléptető-rendszert írni. Persze lehet még rajta mit fejleszteni, így önmagában nem sokkal biztonságosabb, mint a tárolt userid + jelszó hash. Azonban ha hozzáírjuk, hogy a user agent-et is tároljuk, ellenőrizzük, már egész jó kis védelmünk lesz, amit már nehezebb feltörni.

HOZZÁSZÓLOK A CIKKHEZ

Kérjük, írja be véleményét!
írja be ide nevét