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í.
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ť?
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???
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?
Jakub Vrána :
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.
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í.
Jakub Vrána :
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.
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“?
Jakub Vrána :
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ě.
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.
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">
<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
Jakub Vrána :
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?
Jakub Vrána :
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×tamp=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?
Jakub Vrána :
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.