Bezpečné přihlašování uživatelů

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

Článek vyšel na serveru Root.cz.

Pomocí protokolu HTTPS lze zajistit šifrovaný přenos všech informací a ideálně se tak hodí mimo jiné pro přihlašovací formuláře. Pokud tento protokol nemůžete použít (u malých projektů proto, že vám nevyjde vstříc hosting, u velkých z výkonnostních důvodů), přenáší se všechna data nešifrovaně a zdatný uživatel je může po cestě odposlouchávat. Jak jistě víte, nedávno byl obětí takového útoku Seznam.cz. Bezpečné přihlašování se ale dá zajistit i na nezabezpečeném protokolu.

Technika výzva-odpověď funguje tak, že server pošle klientovi výzvu, klient k této výzvě připojí své heslo a serveru pošle otisk tohoto spojení. Server na své straně provede totéž a pokud výsledky odpovídají, tak uživatele přihlásí, jinak ho odmítne. Bezpečnost tohoto řešení je založena na tom, že server každou výzvu posílá jen jednou a pokud se útočníkovi podaří odpověď klienta zachytit, k ničemu mu to neposlouží, protože stejnou výzvu už server nikdy nepošle.

Realizace pomocí PHP, MySQL a JavaScriptu

K technické realizaci tohoto řešení budeme potřebovat na straně serveru i klienta funkci na výpočet otisku hesla spojeného s výzvou. V PHP i dalších serverových jazycích jsou hashovací funkce k dispozici již v základu, takže máme situaci poměrně jednoduchou, na straně klienta budeme muset sáhnout po externí knihovně – např. JavaScript pro MD5 i SHA-1 nabízí v BSD licenci Paul Johnston.

Pro spojení výzvy a hesla by se dalo použít prosté zřetězení, o něco bezpečnější by ale mělo být použití kódu HMAC. V JavaScriptové knihovně je tento algoritmus už implementován, v PHP si funkci budeme muset napsat sami, naštěstí je poměrně jednoduchá:

<?php
/** Výpočet HMAC_MD5
* @param string klíč
* @param string data
* @return string 32 hexadecimálních číslic
*/
function hmac_md5($key, $data) {
    $blocksize = 64;
    if (strlen($key) > $blocksize) {
        $key = pack("H*", md5($key));
    }
    $key = str_pad($key, $blocksize, chr(0x00));
    $k_ipad = $key ^ str_repeat(chr(0x36), $blocksize);
    $k_opad = $key ^ str_repeat(chr(0x5c), $blocksize);
    return md5($k_opad . pack("H*", md5($k_ipad . $data)));
}
?>

Místo pack("H*", md5($s)) lze v PHP 5 použít md5($s, true). Pro výpočet HMAC lze použít také balíček Crypt_HMAC. Od PHP 5.1.2 lze použít také funkci hash_hmac.

Dále musíme zajistit ukládání použitých výzev. Pokud nám nevadí, že výzvy budou ze spojité řady (takže kdokoliv bude moci poznat, kolikrát se náš přihlašovací formulář použil), stačí nám k tomu jednoduchá tabulka:

CREATE TABLE challenges (
	id int NOT NULL AUTO_INCREMENT,
	created datetime NOT NULL,
	PRIMARY KEY (id)
);

Do této tabulky budeme při každém zobrazení přihlašovacího formuláře vkládat nový řádek. Při jeho odeslání se do této tabulky podíváme a pokud v ní výzvu nalezneme, tak ověříme heslo uživatele. Pokud souhlasí, tak výzvu smažeme, což je možné provádět i u zastaralých řádků (např. starších než 1 den), aby velikost tabulky zůstávala v rozumných mezích.

Zbývá vytvořit HTML formulář a celé to spojit dohromady:

<script type="text/javascript" src="md5.js"></script>
<script type="text/javascript">
function md5form(f) {
	f['password_hmac'].value = hex_hmac_md5(hex_md5(f['password'].value), f['challenge'].value);
	f['password'].disabled = true;
	f.submit();
	f['password'].disabled = false;
	return false;
}
</script>
<form action="" method="post" onsubmit="return md5form(this);">
<fieldset>
<?php
mysql_query("INSERT INTO challenges (created) VALUES (NOW())");
$challenge = mysql_insert_id();
?>
<input type="hidden" name="challenge" value="<?php echo $challenge; ?>" />
<input type="hidden" name="password_hmac" value="" />
Login: <input name="login" />
Heslo: <input type="password" name="password" />
<input type="submit" value="Přihlásit se" />
</fieldset>
</form>

Při zapnutém JavaScriptu se do skrytého formulářového pole password_hmac vloží otisk kombinace výzvy a MD5 hesla. MD5 hesla se používá proto, že na serveru je vhodné mít z bezpečnostních důvodů uložen pouze otisk hesla, takže při použití samotného hesla by server neměl jak spočítat výsledný otisk. Poté se zakáže pole se zadaným heslem (což způsobí, že se toto pole s formulářovými daty nebude posílat) a formulář se odešle. Po jeho odeslání se pole s heslem opět povolí, což se dělá jen kvůli tomu, aby se uživatel po neúspěšném přihlášení mohl vrátit v historii a heslo opravit. Pokud má uživatel JavaScript vypnutý, přenese se heslo jako prostý text, pomocí značky <noscript> je možné ho na toto riziko upozornit.

Na straně serveru můžeme heslo ověřit tímto kódem:

<?php
$logged = false;
$row = mysql_fetch_assoc(mysql_query("SELECT password_md5 FROM users WHERE login = '" . mysql_real_escape_string($_POST["login"]) . "'"));
if ($_POST["password_hmac"]) {
    $valid = (hmac_md5($row["password_md5"], $_POST["challenge"]) == $_POST["password_hmac"]);
} else {
    $valid = ($row["password_md5"] == md5($_POST["password"]));
}
if ($valid) {
    mysql_query("DELETE FROM challenges WHERE id = " . intval($_POST["challenge"]));
    if (mysql_affected_rows()) {
        $logged = true;
    }
}
?>

Pokud klient poslal pole password_hmac, ověříme heslo na jeho základě, jinak se spokojíme s textovým tvarem hesla. Za přihlášeného uživatele označíme tehdy, pokud souhlasí hesla a v tabulce challenges se nám podaří smazat zaslanou výzvu.

Poznámka k heslům s diakritikou

Funkce charCodeAt, kterou používá JavaScriptová knihovna, pracuje s Unicodovými kódy znaků. Knihovna z těchto kódů ve výchozím nastavení bere jen spodních 8 bitů (takže řada znaků koliduje), snadno se dá ale přenastavit tak, aby pracovala se 16 bity. Pokud již ale máte uložené otisky hesel uživatelů v jiném kódování, musí se kódování poměrně pracně převést – knihovna MD5 upravená pro Latin-2.

Závěr

S vynaložením nijak zvláštního úsilí můžeme zabezpečit své přihlašovací formuláře proti odposlechu. Použití HTTPS má samozřejmě i nadále svůj smysl, protože jednak šifruje všechna přenášená data a jednak dovolí ověřit i identitu protistrany. Technikou výzva-odpověď se ale dá bezpečnost přihlašovacích formulářů zlepšit i tam, kde použití HTTPS není z jakéhokoliv důvodu možné.

Kromě bezpečného přenášení hesla je vhodné zaměřit se i na jeho bezpečné ukládání na straně serveru a na vhodný způsob pamatování informace o přihlášenosti uživatele. Odchytávání hesel a jejich zveřejnění je ale jistě mediálně nejvděčnější…

Doplnění

Jak správně poznamenali čtenáři v diskusi, je tato technika velice citlivá na bezpečnost dat uložených v databázi. Proto nabízím její vylepšení:

  1. U každého uživatele bude uložen login, challenge a md5(hmac_md5(password, challenge)).
  2. Při přihlašování se AJAXem zjistí, jaký challenge uživatel naposledy použil, a pošle se hmac_md5(password, old_challenge) a md5(hmac_md5(password, new_challenge)).
  3. Na serveru se navíc ověří, jestli md5(old_hmac) souhlasí s tím, co je uloženo v databázi, a pokud ano, přepíše se to novými hodnotami.

Autorem této myšlenky je Paul Jonhston. Přikládám Proof of Concept.

Posílání výzev ze souvislé řady má kromě již zmíněné možnosti zjištění počtu zobrazení přihlašovacího formuláře ještě jednu nevýhodu – skript se může stát obětí útoku DoS. Pokud tomu chceme zabránit a nechceme si navždy pamatovat všechny náhodně vygenerované výzvy, můžeme výzvu ukládat do session proměnné.

Jistou nevýhodou je, že poslání hmac_md5(password, old_challenge) a md5(hmac_md5(password, new_challenge)) dovolí útočníkovi se znalostí hesla toto heslo změnit, aniž bychom to mohli detekovat a uživatele třeba upozornit.

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

Jakub Vrána, Výuka, 12.4.2006, diskuse: 68 (nové: 0)

Diskuse

emu:

nezda se mi, ze by seznam byl obeti, obet podle me bylo 8 poluzaku modraka...seznam se stal maximalne obeti bulvaru

ikona Jakub Vrána OpenID:

Seznam svůj přihlašovací formulář neměl dostatečně zabezpečený, proto se stal obětí. Že to Nova nafoukla, je samozřejmě jiná věc.

Tomáš:

Blbost seznam ten člověk použil sniffer a za to nijak seznam nemůže. To je věc jedině té sítě ke které byli přihlášeni napanení uživatelé!

Marcus Flintus:

myslim, že asi nemá smysl vysvětlovat že se seznam nestal obětí, a že je prakticky nemožné se mezi data seznamu dostat (pokud nejste třeba jeden z jeho programátorů). Už jenom to klasické schema napovida

můj komp _ router _ server_________ . . . _________ WEBSERVER-SEZNAM _ DBSERVER-SEZNAM

- to je zjednodušení situace, a když si vezmeš na jakejch úsecích tady ta komunikace nemůže probíhat...

- hacknuti přihlašovacího formuláře je snad největší blbost co jsem slyšel.

- jedine, jak by se to dalo háknout je nahackovat 10 mašin mezi tebou a seznamem a na všech získat adminský práva. Ale takovejch lidí, co by to byli schopni to udělat je na světě pár. Zrovna jedoho znam, ale ten by neco takoveho neudelal - je na te svetle strane...

ikona Jakub Vrána OpenID:

Přihlašovací formulář Seznamu skutečně nebyl zabezpečen ani proti odposlechu. Toho se dá dosáhnout buď používáním HTTPS nebo alespoň zde popsaným způsobem. Díky této kauze si z toho vzali ponaučení a přihlašovací formulář už směřují na HTTPS. Jak ale podotýká http://www.root.cz/clanky/bezpecne-prihlasovani…/nazory/86670/ Michal Ludvig, tak v případě možnosti změny přenášených dat je to nezachrání. Přes HTTPS by se totiž měl přenášet i přihlašovací formulář a jediný z tohoto pohledu bezpečný způsob přihlášení je přes https://email.seznam.cz/.

Er3kCz:

Jezisi o cem to tu kecas proboha?

Marcus Flintus:

btw. cela ta afera byla o tom ze jeden lamer před 2 rokama vyposlouchal síťovou komunikaci v jedny počitačovy učebně v jedny škole. Pak si ten soubor s heslama uložil na svuj web aby si ho mohl doma stáhnout. a tak ho tam nechal a dočista na něj zapomněl. A co se nestalo! za rok a půl přišel MrGoogle a soubor našel, během pár měsíců to zjistila nova a udělala z toho "nabourání do mašin seznamu" tss...
---

Petr:

Chtěl bych se jen zeptat, jak poté, co nejbezpečněji zajistit přechod mezi stránkami po přihlášení?

krteczek:

Jen podotknu že jsem na seznamu viděl cca před rokem a půl vesele řádit jednoho vtipálka, který si na webzdarma umístil script fungující asi takhle:
rozšířil mezi obětmi zprávu o tom, že vytvořil stránku, kde po zaregistrování a zadání přihlašovacích údajů na seznam, byla dotyčným uložena v prohlížeči cookie pomocí které je identifikoval a automaticky přihlašoval na seznam...
a těch kteří na to skočili bylo dost... a on měl přihlašovací údaje od mnoha schránek...
je to prostě o blbosti lidí...

Spiedy:

Nebylo by lepsi u odeslaneho loginu doplnit <? addslashes ?>

Preci jenom co se s SQL dotazem stane, kdyz nekdo zada
' OR 1=1 OR login='a
tak mu to vrati seznam vsechn hesel :)

ikona Jakub Vrána OpenID:

Skripty očekávají zapnutou direktivu magic_quotes_gpc, je to napsané v patičce všech stránek.

ikona Jakub Vrána OpenID:

Kód jsem předělal na magic_quotes_gpc = Off a zmínil jsem to v patičce.

JFK:

Prihlaseni pokud heslo existuje je jasne, ale jak prenest heslo po registraci, aby pripadny utocnik (viz odposlouchavani po siti) ho nemohl zjistit. Pokud pouziji javascript ke kodovani, neni tezke zjistit jakym zpusobem.

ikona Jakub Vrána OpenID:

Postup je popsán na konci článku v části Doplnění.

Pavel:

zdravim,
hledal jsem jednoduchy challange/response, az jsem narazil tady na tento web.

v podstate mate zde vse, nemel by tedy byt problem jak to rozchodit.

pouzil jsem vas HTML formulář, a misto generovani challange z DB si ho generuji vlastnim scriptem. a v $_POST['password'] po odeslani tohoto formulare nachazim prazdno. v podstate, ta promenna ani neni definovana.

nevim jestli mam vadny nebo jiny md5.js soubor nebo jestli mam chybu nekde jinde v JS.
Ale pokud by to byla alespon trochu mozne byl bych rad kdyby mi nekdo poslal funkcni exampl.

potreboval bych nejaky exampl ktery nejdrive prozene password md5, pak jej zretezi s $challenge a opet prozene md5. cele se to odesle jako $_POST[password].
nevim si uz rady jak mam dale pokracovat, zaroven nechci posilat psw hole.

predem dekuji za jakekoliv exampl muj email nebo radu k vysledku shigi04[zavinac]seznam[tecka]cz
s pozdravem pavel

Pavel:

no uz jsem na to prisel: $_POST['password_hmac']

Hejda:

Proč se tady získává old_challenge AJAXem z challenge.php a ne SQLkem přímo z DB? :)

ikona Jakub Vrána OpenID:

Protože dopředu nevíme, který uživatel se bude přihlašovat.

Author:

Co si myslite o pouziti timestamp misto id? Ted mam na mysli pri pouziti v puvodni verzi bez AJAX. Prece jenom timestamp se meni rychleji, nez id ze souvisle rady a riziko duplicity je snad na hodne nizke urovni (prubeh scriptu ve stejnou vterinu pro vice nez jednoho uzivatele). Zaroven je mozne z db vypustit sloupec created, protoze timestamp interpretuje cas zobrazeni formulare take. Je mozno pouzit i funkci microtime() pro vyssi presnost a tim uz prakticky znemoznit vytvoreni duplicitnich zaznamu v db.

ikona Jakub Vrána OpenID:

Spíše než timestamp bych doporučil uniqid(). Potom je možné tuto hodnotu ukládat jen do session proměnné. Riziko duplicity sekund je pro toto použití příliš vysoké.

Ali:

Měl bych dotaz ten md5.js se dá někde stáhnout,popřípadě je součástí nákého z prohlížečů či byl vámi vytvořen? Zkouším tento příklad na svém počítači pomocí lokalního serveru EasyPHP a nefunguje mi a mám pocit že to je právě díky vadnému  či chybejícímu souboru m5.js. Předem dík za odpověď.

ikona Jakub Vrána OpenID:

http://pajhome.org.uk/crypt/md5/ - odkaz je uveden v článku.

Martin:

Ahoj,
tak mě tak napadlo, jak čtenáři správně poznamenali v diskuzi, kde se dá přijít na MD5 hash? Napadá mě, že pouze z databáze (protože z klienta se neposílá hash samotného hesla). Takže, nestačilo by ukládat hesla do databáze právě použitím algoritmu RSA? Kdyby někdo ukradl obsah databáze, RSA šifry mu jsou bez klíčů k ničemu. Kdyby někdo ukradl hodnotu posílanou na server, tak je mu to taky k ničemu, protože odezva platí vždy jen na jeden požadavek. A vlastní hodnotu posílanou na server si útočník vytvořit nemůže, protože nezná MD5 hash - případná zašifrovaná podoba hesla z databáze je mu k ničemu.
Šlo by to tak?

Miro:

Ahoj Jakube.
Pracujem teraz na novom projekte a tak som sa rozhodol, ze trochu pozmenim system prihlasovania, tak som vyuzil tento tvoj challenge-response a podla prilozeneho Proof of concept som to rozbehal.. udrzovanie informacii o prihlaseni som tiez realizoval tvojou metodou popisovanou v http://php.vrana.cz/prihlasovani-uzivatelu.php. Ja som to predtym riesil tak, ze informacia o prihlaseni bola ulozena v db, ale myslim, ze tvoja metoda celkom postacuje, takze nemusim klast zbytocne dotazy do db. No a ty v tvojom system pouzivas session_regenerate_id(). mas ho pouzite po uspesnom prihlaseni. no a sa chcem spytat, ze ci ho staci zavolat iba na tomto mieste, po prihlaseni, alebo ho treba/je lepsie ho volat po kazdom nacitani stranky, ked je uz uzivatel prihlaseny?
A este k tomu challenge-response. Chcel som prihlasovaci formular umiestnit tak, ze by sa stale zobrazovat na stranke, niekde na bocnom paneli, user by si browsoval stranku a ked sa bude chciet prihlasit, tak ho ma hned poruke a prihlasi sa. No lenze to by pri kazdom nacitani stranky, cize aj ked by si niekto pozeral stranku, cital clanky atd. a vobec by sa nechcel prihlasit, stale by sa kladol dotaz do db a stale by sa zvysovala hodnota challenge. To sa mi zdalo asi nie moc dobre, tak som prihlasovanie prelozil do samostatneho suboru a ked sa user chce prihlasit musi sa prekliknut na inu stranku, co mi pripada trochu neprakticke.. tak rozmyslam, ze ci by nebolo lepsie generovat challenge nejak inak alebo nevadi, ked po par mesiacoch bude mat challenge hodnotu napr. 1534341354214 ;)

ikona Jakub Vrána OpenID:

session_regenerate_id() je vhodné volat na každém místě, kde uživatel získá nějaká nová oprávnění - tedy typicky právě jen po přihlášení.

Vysoký challenge ničemu nevadí. Nicméně pokud používáš sessions, tak ho nemusíš ukládat do databáze, ale můžeš použít session proměnnou.

Miro:

Viem, ze vysoky challenge nicomu nevadi, len nejak tak sa mi to trochu nepozdavalo, ze bude stale narastat a narastat.. aby nahodou po case nepresiahol 2 147 483 647 ;).. ale asi si ho fakt zacnem generovat v sessions..

Miro:

a este mam jeden problemik ktory sa mi vyskytuje pri praci so sessions: prihlasim sa do mojej aplikacie vyuzivajucej sessions, potom taku hodinu alebo aj viac nic nerobim, potom kliknem na nejaky odkaz a som automaticky odhlaseny, pretoze sa cele pole sessions vymaze, takze aj udaj o prihlasenosti.. a nie som si isty, kde sa v cfg nastavuje cas, pocas ktoreho php uchovava premenne pre dane sedenie.. je to session.gc_maxlifetime ? aka je optimalna hodnota?.. dakujem

pokker:

trochu oneskorene ale preco vysoky challenge  ? co pouzitie
napriklad md5(rand(0,100000)); ???? nechapem ja som v scripte generoval nahodne retazce prave z rand'u je na tom nieco zle ?

LPL:

Nevím jestli jsem udělal něco špatně, ale když porovnám výsledek JS scriptu(hex_hmac_md5) s výsledkem PHP funkce hmac_md5() dostanu různé hodnoty. Čistý MD5 funguje správně, ale HMAC se liší. Můžete prosím někdo poradit?? Dík

ikona Jakub Vrána OpenID:

Uveď alespoň hodnoty, které používáš, a taky platformu klienta a serveru (především zda je operační systém 32- nebo 64-bitový).

LPL:

OK, sorry, ale nemuzu ted pozuit diakritiku. Po tve otazce, me napadlo zkusit to na 64-bitovym OS, protoze moje testy byly na starym Win98 a nasel jsem alespon stejne vysledky, ale pouze v pripade, ze 'salt'(data) byl povazovan za retezec, nikoliv int. Z toho tveho prikladu jsem to pochopil jako ze salt muze byt pouze cislo (viz. id_challenge) a cpal jsem do obou - PHP, i JS, stejne cislo. ...Ceka me jeste hodne objevu. Diky za reakci

ikona Jakub Vrána OpenID:

Funkci hex_hmac_md5() se musí předat řetězec, funkce hmac_md5() si řetězec z čísla sama vyrobí. V příkladu je f['challenge'].value vždy řetězec, i když je obsahem číslo. Pro použití čísla by bylo potřeba napsat třeba +f['challenge'].value.

Elijen:

Tahle diskuse je už asi mrtvá, ale stejně ...

Možná jsem něco přehlédl, ale podle mě tahle je tahle implementace NEFUNKČNÍ!

Na straně serveru se v implementaci výš používá pouze challange obdržený POSTem od klienta, což utočníkovy umožňuje odposlechnout libovolný challange a k němu patřící hmac a pak vnutit serveru svůj odposlechnutý challange s týmž hmacem a loginem uživatele => Efekt hashovnání == NULL ;-)

ikona Jakub Vrána OpenID:

Přehlédnul jsi kód
<?php
if (mysql_affected_rows()) {
    $logged = true;
}
?>
Pokud výzva v tabulce nebyla, nepodaří se ji smazat a proměnná $logged se tedy nenastaví.

Alu:

V tomto kódu vidím drobnou (ale skutečně drobnou) bezpečnostní trhlinku v tom, že kdyby neexistovala tabulka challenges (někomu se povedlo ji smazat), funkce mysql_affected_rows() by vrátila hodnotu -1 a tudíž by došlo k přihlášení. Kdybych to psal já, určitě bych pro jistotu i tento případ ošetřil...

kolio:

Ahoj Jakube,

Omlouvam se za cestinu. Ulozil jsem soubory ze challange.zip na serveru, vytvoril databazi atd.
A zacal vyskouset ...
Ulozil jsem 2 uzivatele- a (heslo: "a"), b (heslo: "b")
Potom jem otevrel u IE7 2 Tabs u stejneho prohlizece a jsem zacal prihlasovat uzivatele: v prvni "Tab" jsem prihlasil "a" v druhem "Tab" "b" a opakoval - pri prvnim opakovani uzivatele se neprihlasili a v tabulce "users" se ulozili takove password_hmac_md5, ze uz se neda prihlasit.

V cem je problem - z toho, ze pouzivam  1 prohlizec a neco se plete se sessionem. Jak se to da upravit? Nebo ja neco delam blbe?

kolio:

Jakube rekni neco, cekam na to uz 9 mesicu :)
Asi otazka je hloupa, rekni alespon proc je to tak :)

kolio:

Je to podle me diskriminace, ostatnim odpovidas, me ne.

ikona Jakub Vrána OpenID:

Toto není soukromá poradna, ale veřejná diskuse. Odpovědět na tvůj dotaz může kdo chce. Nikdo na něj asi odpovědět nechce.

kolio:

Konecne - nejaka reakce! Je to uspech - za 9 mesicu. Ne rozumim proc jsi ochotni polemizovat formalni otazky jako co je ten forum a t.d. a nejsi ochotni odpovedet na konkretnou otazku tikajici se tveho clanku. Ja bych odpovedel i v kratce. Navic zda se mi ze jestli clovek pouzije tvuj model prihlasovani zpusobem jak jsem popsal v dotazu ne funguje to. Proc ne funguje? Kdyz nechces  odpovedet me rekni to alespon verejnosti :)

DPetr:

Ahoj Jakube,

mám takovou hloupou otázku. Pozoruji dění kolem bezpečnosti hesel a přihlašování a snažím se přijít na nějaké zjednodušení. Zdá se mi že většina řešení se snaží obejít nebezpečí decryptace hesla z nějakého hashe a zachycení komunikace o přihlašování . Ten druhý problém je nejjednodušeji řešitelný pomocí ssl přístupu ke stránkám . Není to dokonalé ale jako základní obrana stačí. Většinou se ale řeší problém unikátních hashes z hesla. Nebylo by jednoduché řešení použít dvojnásobný hash? myslim md5(md5(heslo)) popřípadě kryptaci hesla nejdřív sha1 a pak md5 či jakoukoli jinou kobinací dvou kryptovacích procesů? vím že s trochou času a dostatečně silným PC se dá databáze md5 hashes pro alfanumerická hesla do 8 znaků není těžko sestavit tabulku zpětného převodu. ale tabulku nějakých 6 nejčastějších kryptovacích algoritmů z md5 hashe dat si člověk jen tak nesestaví. Mám pravdu aspoň částečně nebo plácám kraviny?

Díky předem za odpověď
Petr

ikona Jakub Vrána OpenID:

Předpočítané tabulky lze odstavit přidáním saltu - to je správný a bezpečný způsob: http://php.vrana.cz/ukladani-hesel.php. Použití nezvyklé hešovací funkce (do čehož spadá i dvojité použití MD5) tabulky samozřejmě taky odstaví, ale neřeší další neduhy (např. že dva uživatelé se stejným heslem mají stejný heš).

Opakovaná aplikace hešovací funkce bezpečnost nezvyšuje, čistě teoreticky ji může naopak snížit, vysvětlení je ale o něco složitější. Jde o to, že v hešování nejspíš budou vznikat cykly, kterými se zmenšuje prostor hesel, která je potřeba prozkoumat.

Krkel:

Ahoj, snažím se pochopit princip tohoto návrhu, ale nejde mi do hlavy. Můžeš mi Jakube prosím říct, čemu přesně toto řešení zabranuje. Díky

j.:

Zdravím, chtěl bych se zeptat, co je to this v :

return md5form(this);

Pak ještě jak píšeš " Na straně serveru můžeme heslo ověřit tímto kódem:....." Tak to je myšleno při ověření na začatku nebo pak i v průběhu pohybu mezi stránkami namísto session&cookies ?

Moc dík za odpověď.j.

ikona Jakub Vrána OpenID:

this je odkaz na aktuální objekt, v obsluze události formuláře tedy na formulář.

Ověření hesla probíhá jen na začátku, pak už pomocí session.

j.:

A ještě jedna otázečka, kam mam ve formuláři tus tránku, na kterou to bude pokračovat po tom ověření např. neco.php Diky za odpověď. j.

Vojtěch Dobeš:

Zdravím. Rád bych se zeptal, zda-li existuje nějaké spojení challenge/response metody při přihlašování a metody solení otisků hesel náhodně generovaným saltem při registraci. Při googlení jsem nalezl návrh použít jako salt např. uživatelské jméno - je to oproti náhodně generovanému saltu větší bezpečnostní riziko?

ikona Jakub Vrána OpenID:

Login není špatný adept, ale není ani ideální. Pokud může být login krátký, tak jeho přidání heš dostatečně nezesložití, takže pro jeho rozlousknutí půjdou lépe využít Rainbow tables.

ikona Marek:

Ahoj, mohol by si sa pozrieť na ten priložený Proof of Concept na riadok 10 v súbore login.php:
<?php
$password_hmac_md5
= md5(hmac_md5($_POST["password"], $_POST["challenge"]));
?>
Ide o prihlásenie pri vypnutom javascripte, nejak sa mi nezdá to použitie $_POST["challenge"] keďže taká položka sa neodosiela cez formulár, a následne sa zapisuje do databázy iná hodnota na 16-tom riadku ten istý súbor:
<?php
mysql_query
("UPDATE users SET challenge = '$_SESSION[challenge]', password_hmac_md5 = '$password_hmac_md5' WHERE login = '$_POST[login]'");
?>
Rozdiel je v tom, že HASH hodnota hesla je tvorená z $_POST["challenge"]="" ale do stĺpca challenge sa uloží hodnota $_SESSION[challenge]="hodnota". Potom by mal nastať problém pri ďalšom overení hesla, keďže pri použití challnege z DB nedostaneme rovnaký odtlačok ako je uložené heslo. Snáď som čosi neprehliadol.

ikona Jakub Vrána OpenID:

Díky za upozornění, skutečně to bylo špatně. Opravil jsem to.

Tori:

Dobrý večer,
mohl byste se podívat na JScriptovou funkci hex_hmac_md5()? Jejich autor zřejmě použil opačné pořadí parametrů, než je u obdobné funkce v PHP, tedy hex_hmac_md5(key, data). Omlouvám se, pokud jsem něco přehlédla.

ikona Jakub Vrána OpenID:

Pokud máš na mysli funkci hash_hmac(), tak pořadí parametrů je skutečně opačné.

majo:

Ahoj Jakub,

napadla mna jedna vec. Ak su v databaze ulozene hesla osolene (rovnakov solov + login), tak potom musim tiez do prehliadaca poslat tuto sol aby to fungovalo, cim ju vlastne prezradim. Ak nastane kompromitacia DB, utocnik ma v podstate v rukach mechanizmus tvorby hashu a moze zacat generovat rainbow tables a nasledne uspesne porovnavat (aspon tie jednoduche hesla). Alebo som to nepochopil spravne?

ikona Jakub Vrána OpenID:

Na saltu nic tajného v zásadě není. Slouží k tomu, aby se nedaly používat stávající vygenerované tabulky.

majo:

to ano, ale pokial utocnik nepozna sol, nema sancu ani na brute-force pre neho zaujmaveho uzivatela (ak bude mat slabe heslo, co nie je nic nezvycajne). Ako ochrana pred touto malilinkou slabinou ma este napadlo (symetricky) upravit hash nejakym vlastnym algoritmom, a pred porovnanim tento hash zrekonstruovat. Alebo ist uz konecne liecit tu paranoju :-D.

Samuel:

Myslím že by bolo vhodné trochu pozmeniť javascript, totižto chrome odosiela aj disabled input, teda aspoň aktuálna 9. verzia. Špecifikácie sú špecifikácie ale ja by som sa na nejaký disabled nespoliehal :) a už vôbec nie keď na tom padá celá myšlienka neprenášania plain hesla. Inak úplne super, ďakujem za tento článok, zasa som sa niečo naučil o bezpečnosti, síce som ešte presne nepochopil myšlienku toho doplneného scriptu ale silno na tom pracujem. Veľmi by som ti Jakub bol vďačný keby si tie 3 body trochu viac rozobral. Ďakujem

Samuel:

Aha tak som sa sekol :) predsa neodosiela disabled ale odošle stav formuláru v akom sa nachádzal až po ukončení funkcie md5form(f) (kedy je f['password'].disabled už znova true) a nie stav formulár pri volaní f.submit();

podobný problém aj v safari

Jano:

Zdravím, našiel som túto diskusiu, skopíroval všetko navrhované, spustil Fiddler (Web debugger), spustil stránku a ťukol na položku vo Fiddler-i, ktorou bol poslaný submit "Přihlásit se" ... a veselo si prečítal login=.... password=.... Neviem, či sa tu iba sranduje, ale zaručene nebudem mať záujem o propagované školenia, pane Vrána.

ikona Jakub Vrána OpenID:

Pravděpodobně jste to zkopíroval špatně. Případně jste to pustil s vypnutým JavaScriptem.

Na školení byste se dozvěděl, že než něco jenom zkopírovat, je lepší to nejprve pochopit. Pak je i o něco snadnější zjistit, kde je jádro případných problémů.

Jano:

Obávam sa, že nejde o NEpochopenie z mojej strany. Okopíroval som všetko správne. Problém je jednoducho v tom, že nezávisle na type="hidden" atribúte pre name="password" položky formuláru POST pošle jej hodnotu (všetko presne v súlade s normami) nazávisle na nastavení disabled=true . Pokiaľ nemá byť táto položka poslaná, mala by byť bez mena, iba s id="password", nakoľko v takom prípade by sa v POST-e nemala objaviť (normou nie som si istý, ale funguje to tak napr. v Google Chrome).
JavaScript funkcionalitu f['password'] to nijako nenaruší, nakoľko to vypadá tak, že interne sa document prehľadáva nielen getElementByName, ale aj getElementById.
Nech mám v náznaku vysvetlenia pravdu, či nie, po mojej úprave som už vo Fiddler-i videl iba login=..., ale nie password=...

ikona David Grudl OpenID:

Nikoliv, prvky které jsou disabled se dle specifikace neodesílají. Konkrétně to říká věta "Disabled controls cannot be successful" na http://www.w3.org/TR/html401/interact/forms.html#h-17.12

Dominik Bureš:

Zdravím, rád bych položil dotaz, nejspíš trochu mimo soudek. Je v dnešní době bezpečné - dostačující; stále používat MD5?

Troopy:

Dik za radu. Tato metoda se mi libi. Narazil jsem ale na problem u prohlizece google chrome. Z nejakeho duvodu mi nacita kazdou stranku 2x. Kdyz bych napr. Udelal stranku s citanim zobrazeni, pricte se pokazde pri refreshi 2. To co mi zobrazi chrome odpovida prvnimu zobrazeni. To zpusobi, ze mam v nactenem formulari prvni challenge, ale v databazi na me ceka druhy, uplne jiny challenge. Setkal se nekdo s timto problemem? Dalo by se to ruzne zaplatovat, ale dal bych prednost zamezeni dvojiteho nacitani. Predem dekuji za pripadne odpovedi.

Roman:

Takhle by se mohl chovat, pokud bys v odkazech používal http a na serveru měl v .htaccess automatické přesměrování na https. Takže každý dotaz na http by vedl k přesměrování na https (-> 2 requesty). Koukni se do vývojářské konzole (F12), záložka "network", třeba tam na něco přijdeš.

ap:

Zaujíma ma situácia, keď by niekto získal tabuľku užívateľov s md5 heslami. Potom ešte útočník potrebuje vedieť skutočné heslo?

Analytik:

Dobrý den, jak máte

<?php
if ($valid) {
   
mysql_query("DELETE FROM challenges WHERE id = " . intval($_POST["challenge"]));
    if (
mysql_affected_rows()) {
       
$logged = true;
    }
}
?>

Nebylo by lepší u podmínky if (mysql_affected_rows())
použít raději if (mysql_affected_rows()>0)

?

ikona Jakub Vrána OpenID:

Lišilo by se to jedině v případě, když by to mohlo vrátit záporné číslo. Takže je možné použít verzi, která člověku přijde čitelnější.

Analytik:

co by pak udělala Vaše podmínka, kdyby byla vrácena -1 ?

Pokud se požije  if  (mysql_affected_rows()>0), tak by se neprovedlo $logged = true; a tím pádem by neproběhlo přihlášení.

Děkuji.

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.