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: 22 (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.

PeTaX:

Nebylo by, Jakube, vhodné při UPDATE hesla unsetnout i sloupec `heslo_token_platnost`? Nebo je lepší obecně skenovat ten sloupec tabulky anonymně a mazat všechny prošlé časy? Anebo to klidně „nechat muset být“?

ikona Jakub Vrána OpenID:

Když není nastavené heslo_platnost, tak na hodnotě heslo_token_platnost nezáleží, takže je podle mě lepší ho nechat být, protože se to může třeba hodit při analýze problémů a podobně.

ikona Jaromír:

Dobrý den,
aktuálně řeším problém zapomenutého hesla na webu. Pročetl jsem si zdejší návod, a mám jednu nejasnost. Pokud se útočník "zmocní" obsahu emailu, ve kterém uživateli posílám url adresu s uvedeným loginem a tokenem, tak přece je pro něj hračka účet na webu ukrást jednoduchou změnou hesla. Jak se tedy tato metoda liší od poslání hesla do mailu přímo? Nebo jsem něco přehlédl? Děkuji za osvětlení problému.
Hezký den.

ikona Jakub Vrána OpenID:

Při správném ukládání hesel (http://php.vrana.cz/ukladani-hesel-bezpecne.php) provozovatel heslo v první řadě ani nezná. Druhá věc je, že token platí jen omezenou dobu, ale heslo platí stále, takže útočník má na útok kratší dobu.

libor:

Zdravím

mám tento kod na obnovu hesla

<?
echo "<br />";
$neramail=0;
$show=1;
if (isset(
$_POST['subforgot'])) {
   
$querys=mysql_query("SELECT * FROM aff_ausers where email='{$_POST['email']}'");
    if (
$row = mysql_fetch_array($querys)) {
        echo
"<br /><br /><p align=center class=pavblue style='margin-left:20'>Vaše prihlašovací údaje byli zaslané na <b>{$_POST['email']}</p></b>";
        include(
"letters/forgot.php");
       
$show=0;
    }
    else echo
"<p align=center class=pavblue style='margin-left:20'>Účet s tímto emailem <font color=red>{$_POST['email']}</font> nebyl nalezen.</p><br />";
}

if (
$show) {?>
    <p align=center class=pavblack><b>Zapoměly jste přihlašovací údaje? Nevadí!</b></p><br />
    <p align=center class=pavblack><b>Zadajte svůj email a my vám je zašleme na email!</b></p><br />

    <form method="POST" action="<?php echo KL_HTTP_SERVER; ?>index.php?id=forgot">
    <p align=center>Zadejte váš email:
    <input type="text" name="email" size="20">&nbsp;&nbsp;&nbsp;
    <input type="submit" value="Zaslať" name="subforgot">
    </form></p>
<?}?>

Heslo příjde ale to původní..Já bych potřeboval aby se heslo resetovao a poslalo náhodné  třeba 5tgJn8

ikona Jakub Vrána OpenID:

To není dobrý nápad. Udělej to tak, jak radí článek.

libor:

Děkuji za odpověď.To se právě snažím udělat,jsem tak trochu začátečník.Včera my přišla vaše kniha php,tak snad se něco přiučím..
Když tam vložím to php co je nahoře tak my to vyhodí prázdnou stránku.Tak nevím co dělám špatně.

Ondřej:

Dobrý den,

díky za užitečný článek. Mám dotaz k bodu

> 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ž si takovou situaci představím, co by zabránilo útočníkovi s přístupem k úložišti a kódu ve vygenerování md5 daného tokenu, případně rovnou ve změnění hesla daného uživatele úpravou sloupce user.password v úložišti?

ikona Jakub Vrána OpenID:

Pokud má útočník práva zápisu, tak tam rozdíl není. Pokud může útočník jen číst (případně má přístup k mirroru nebo čerstvé záloze databáze), tak tam rozdíl je.

Robert:

Většinou to řeším tak, že mailem pošlu link, který je tvořen nějak takto:
https://www.web.cz/zmenahesla.php?user=abc@fima.cz&timestamp=1637367601&signature=xxxxx

Kde signature je base64_encode(password_hash($user.$timestamp.$tajnyString, PASSWORD_DEFAULT))

Na stránce zmenahesla.php si pak pomocí password_verify($user.$timestamp.$tajnyString, signature) zkontroluji signature a ověřím jestli timestamp není moc starý. A když je vše ok, tak tomu uživateli abc@fima.cz dovolím změnit heslo.

K tomu $tajnyString se chovám stejně jako k přihlašovacím údajům do db, tj. jsou v configu a různé pro ostrý a vývoj.

Výhodou je, že nemusím nic zapisovat do db, při požadavku na změnu hesla, což může někdy dělat zmatky, když si to uživatel pošle 2x atd...

Snad by to mělo být ok?

ikona Jakub Vrána OpenID:

Není to odolné proti (bývalým) nespokojeným zaměstnancům. Když se zaměstnanec dostane k tomu $tajnyString, tak pak může měnit hesla všem uživatelům, i když už nemá přístup ke zdrojákům ani k databázi.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 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.