Poslání zapomenutého hesla

Školení, která pořádám

Pokud bezpečně ukládáme hesla, tak je uživatelům samozřejmě nemůžeme poslat v případě zapomenutí, protože je nemáme jak získat. Nabízí se ale několik způsobů, jak zapomenutí hesel řešit i v případě jejich bezpečného uložení.

Nevhodný způsob spočívá v tom, že uživateli vygenerujeme nové heslo a pošleme mu ho. To totiž umožňuje útočníkovi opakovaně tvrdit, že zapomněl heslo nějakého uživatele, a tím mu znemožnit přihlášení (nebo alespoň znepříjemnit život tím, že přestane fungovat jeho původní heslo). Jde vlastně o útok DoS.

Lepší je proto vygenerovat uživateli token (náhodný řetězec), pomocí kterého si bude moci heslo po omezenou dobu změnit bez toho, aniž by heslo znal. Tento token mu následně pošleme. Ve zprávě je vhodné zohlednit i situaci, kdy uživatel o změnu zapomenutého hesla nežádal nebo si na něj mezitím vzpomněl (což zachová původní heslo a smaže token).

Při ukládání tokenu na straně serveru se často chybuje, protože se do databáze zapisuje v podobě, která se posílá uživateli. Tím pádem jde o klasické schéma Security by Obscurity – v případě kompromitace úložiště by útočník mohl změnit heslo kterémukoliv uživateli (pro kterého by si pomocí aplikace nejprve vygeneroval token). Když člověk přemýšlí o bezpečnosti aplikace, tak je vhodné za „útočníka“ dosadit programátora aplikace, který má přístup ke všem zdrojovým kódům a kopii databáze (vytvářené třeba pro účely vývojového prostředí). To je nutným předpokladem pro schéma Security by Design. Problém vyřešíme tím, že místo tokenu uložíme jeho haš.

<?php
// zapomenutí hesla
$token = md5(uniqid(rand(), true));
mysql_query("
    UPDATE uzivatele SET
    heslo_token = '" . md5($token) . "',
    heslo_token_platnost = NOW() + INTERVAL 1 DAY
    WHERE login = '" . mysql_real_escape_string($_POST["login"]) . "'
");
mail($email, "Zapomenute heslo", "http://$_SERVER[SERVER_NAME]/obnoveni-hesla.php?login=" . urlencode($_POST["login"]) . "&token=$token");
// na stránce obnoveni-hesla.php zobrazíme formulář pro zadání nového hesla nebo zrušení žádosti

// zrušení žádosti o zapomenuté heslo
mysql_query("
    UPDATE uzivatele SET
    heslo_token = NULL
    WHERE login = '" . mysql_real_escape_string($_GET["login"]) . "'
    AND heslo_token = '" . md5($_GET["token"]) . "'
");

// změna zapomenutého hesla
mysql_query("
    UPDATE uzivatele SET
    heslo_sha1 = SHA1(CONCAT('" . mysql_real_escape_string($_POST["heslo"]) . "', heslo_salt)),
    heslo_token = NULL
    WHERE login = '" . mysql_real_escape_string($_GET["login"]) . "'
    AND heslo_token = '" . md5($_GET["token"]) . "'
    AND heslo_token_platnost >= NOW()
");
?>

Návod na změnu zapomenutého hesla musíme uživateli samozřejmě posílat na adresu uvedenou v době registrace, nikoliv až při zapomenutí hesla. Také je dobré vzít v potaz situaci, kdy uživatel kromě hesla zapomene i své přihlašovací jméno (a dovolit mu tedy zadat adresu, kterou uvedl při registraci). Poslání návodu pro změnu zapomenutého hesla se nejčastěji realizuje e-mailem, ten by ideálně měl být zašifrovaný.

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Řešení problému, 30.6.2010, diskuse: 11 (nové: 0)

Diskuse

jaguar:

Pekný článok!
Ja to riešim obdobne ale na šifrovanie odosielaných mailov (napr. pomocou funkcie mail) v PHP som ešte neprišiel. Je to možné spraviť?

ikona Jakub Vrána OpenID:

Pro šifrování mailů je potřeba znát veřejný klíč příjemce, což je ta největší překážka. Pak už to vyřeší funkce openssl_pkcs7_encrypt().

Jval:

Nevím, ale přijde mi, že druhý a třetí dotaz nebude fungovat. Vždy je tam podmínka WHERE heslo_token kde se mu předhodí $_GET['token'] ale bez md5 hashovani.

Nebo jsem něco přehlédl???

ikona Jakub Vrána OpenID:

No jistě, díky za upozornění. Opravil jsem to.

ikona Joelp:

Já používám namísto md5 fci crypt a dodnes nerozumím tomu, proč ji nepoužívají i jiní.

Vidíš v tom nějaký problém, nebo je to jen zvyk?

ikona Jakub Vrána OpenID:

Jednotlivé varianty crypt() nemusí být k dispozici všude. Takže to, co zahašuješ na jednom serveru, nemusí jít zkontrolovat na jiném serveru.

ikona Joelp:

jo, o tom jsem četl, ale nikdy mě to neselhalo. Dokonce jsem přenášel z 2 roky zastaralého serveru (před 4 roky instalovaný) na fungl nový a vše fungovalo. Tenkrát jsem čekal problém, ale nenastal.

Možná je to ale tím, že jsou to moje vlastní servery, kde používám stejnou distribuci OS.

Francek Vosmrádlo:

Chtěl bych se zeptat - pokud je útočníkem programátor aplikace, který má přístup k databázi, má nějaký význam heslo_salt? (myslím: z hlediska doby, za kterou prolomí heslo.) Pokud ano, v čem?

cucací potřeby:

Imho salt (sůl) je dobrá pro to, aby útočník nemohl porovnávat otisky hesel vůči nějaké tabulce.
Na internetu koluje hafo seznamů obsahujících v průměru stovky nejpoužívanějších hesel. Porovnáním (jejich MD5/SHA1/... otisků) se téměř vždy najde nějaká shoda. (Na samotné porovnání vlastně ani není potřeba žádný extra velký výpočetní výkon - je to skoro zadarmo.) Ovšem, přidáním nějakého náhodného a dostatečně dlouhého balastu za každé heslo (před hašováním) se situace mění a otisky budou i pro stejná původní hesla unikátní.

ikona Jakub Vrána OpenID:

Kromě Rainbow tables, o kterých píše cucací potřeby, je salt důležitý především proto, aby se haše nedaly zkoumat všechny najednou. Bez saltu (nebo pouze s jedním společným saltem) může útočník vygenerovaná hesla porovnat se všemi uživateli najednou.

ikona v6ak:

Přemýšlím, zda by mělo nějaký smysl měnit při změně hesla i salt. Možná ne.
Dále je otázka omezení počtu žádostí o reset hesla na uživatele za den. Jinak to může obtěžovat a případně i zamezit změně hesla.
Jinak mi přijde, že zobrazení formuláře pro změnu hesla namísto vygenerování nového je, bohužel, spíše výjimka. Možná se mýlím, zas tak často reset hesla nevyužívám.

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

© 2005-2012 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.