Několik poznámek k heslům

Přihlášení uživatele k webové aplikaci je přece tak snadné: zadá jméno a heslo, odešle a – voila! – je přihlášen. Ale co je za tím? A co by za tím mělo být? Přinesou nové technologie nějaké změny v téhle oblasti? Oprášíme staré známé technologie, nebo vzniknou nové?

„Uživatelské jméno a heslo“ patří k počítačům už od jejich dávnověku. Je to něco tak samozřejmého, že se nad tím už nikdo ani nepozastaví, a přitom to je klíčová součást bezpečnosti celé webové aplikace. Problém nastává ve chvíli, kdy se nad „jménem a heslem“ nepozastaví ani tvůrce webové aplikace. Pojďme se společně pozastavit nad samozřejmostmi a pustit trochu fantazii na špacír…

Solte!

Údaje o uživatelích bývají uloženy na serveru v nějaké databázi. Součástí údajů je uživatelské jméno, informace o hesle a další informace, podle toho, co aplikace vyžaduje. Záměrně píši „informace o hesle“ a nikoli „heslo“ – o tom, jak hloupé je ukládat hesla do databáze v otevřeném, čitelném formátu, byly napsány už spousty článků. Ve zkratce: nedělejte to; pokud to uděláte, tak se nedivte.

Mnohem lepší variantou je ukládání hesla v podobě hashe – „otisku“, který je výsledkem speciální matematické funkce. Obecně tyto funkce pracují tak, že berou vstupní data a k nim vrací řetězec, který má některé specifické vlastnosti: 1. pro stejná vstupní data je stejný, 2. má konstantní délku, 3. drobná změna vstupních dat vyvolá velkou změnu ve výsledku, 4. je prakticky nemožné nalézt různá vstupní data se stejným hashem a 5. z výsledného řetězce je v praxi nemožné rekonstruovat původní text.

Hashovacích funkcí je mnoho, ale ve webových aplikacích jsou nejpoužívanější funkce z rodiny MD (téměř výhradně MD5) a SHA (SHA1, SHA2). U MD5 byly nalezeny chyby v návrhu, které snižují jeho bezpečnost, takže mnozí tvůrci přechází na SHA algoritmy. Je na místě podotknout, že SHA1 byl rovněž označen za slabší a doporučeno je používat algoritmy SHA2 (SHA256, SHA512 atd.)

Problém bezpečnosti

Klasická situace: útočník se dostane do systému a stáhne si databázi uživatelů (příklad z poslední doby: hack hostingu banan.cz). Pokud jsou uložená hesla v čisté podobě, pak už není co řešit – útočník má databázi jmen a hesel, server je pro něj otevřený a navíc je pravděpodobné, že mnozí lidé budou mít stejné jméno a heslo i na dalších webech.

Pokud jsou hesla hashována, musí je útočník „rozlousknout“. Pravděpodobně použije tzv. „rainbow tables“ – tabulky s vypočítanými hodnotami hashů pro různé řetězce. Tyto tabulky existují pro „slovníková hesla“ i pro kombinace různých znaků o různé délce (např. „pouze malá písmena s délkou 5, 6, 7, … znaků“, „malá písmena a číslice s délkou 5, 6, …“). Pokud má uživatel „známé heslo“ nebo „krátké heslo“, je jeho nalezení v tabulkách otázkou minut.

Časově náročnější je vytvoření rainbow table podle specifikace (kterou server ochotně při registraci prozradí: „heslo musí mít alespoň 5 znaků a musí obsahovat alespoň jedno velké písmeno“). Ovšem díky možnosti pronajmout si velký výpočetní výkon v cloudu, počítat tabulky distribuovaně nebo využít CUDA není takový úkol nemožný.

Do karet hraje útočníkovi i to, že MD5, SHA1, SHA2 atd. jsou algoritmy určené pro rychlý výpočet hashe u velkých vstupních souborů. U MD5 dokáže poměrně běžný server počítat hashe rychlostí větší než 300MB za sekundu. To znamená, že např. kombinace malých písmen a číslic o délce 6 znaků může ověřit za zhruba 40 sekund (zdroj: Coda Hale, viz další text).

Jakou používáte hashovací funkci pro uživatelská hesla?

Solíme!

Čím delší heslo, tím víc času je zapotřebí na výpočet hashů možných kombinací. Tabulku pro čtyřznaková hesla si spočítáte během pár sekund, pro pětiznaková už to bude pár minut, a časová náročnost roste s délkou hesla. Ideální možností by bylo donutit uživatele, aby používali alespoň patnáctiznaková hesla se speciálními znaky, ale takový systém by jen těžko uspěl. Proto se sahá k metodě solení (salt), kdy ke „standardně dlouhým“ heslům na serveru přidáváme dlouhý řetězec. Hash je pak počítán např. pro 40 znaků – a pro tak dlouhé řetězce je vytváření rainbow tables ze všech možných kombinací téměř nemožné.

… ale zbytečně!

Pokud útočník pronikne do systému a získá přístup k databázi, je pravděpodobné, že získá i „salt řetězec“ a zjistí, jak přesně solíme. Podle této šablony si může vygenerovat nové tabulky – se znalostí „solicího řetězce“ je to opět prostá úloha, kdy místo řetězců „aaa“, „aab“, „aac“ počítáme hashe pro „supertajny-salt-aaa“, „supertajny-salt-aab“, „supertajny-salt-aac“… Počet kombinací zůstává stejný jako bez saltu a útočník má třeba během týdne dostatečnou munici na rozlomení většiny uživatelských he­sel.

Protihmatem proti tomuto postupu je variabilní salt – namísto konstantního dlouhého řetězce se používá konstantní dlouhý řetězec a k němu ještě náhodná kombinace znaků. Tu si klidně uložíme pro každého uživatele do databáze v otevřeném textu. Nezáleží na tom, že útočník bude tuhle náhodnou část znát; jde o to, že by musel pro každého uživatele budovat znovu vlastní sadu tabulek, což by bylo časově velmi náročné. (viz Joshua Thijssen: Password hashing and salting).

Jiný postup doporučuje již zmíněný Coda Hale v článku How to safely store a password. Upozorňuje na to, že hashovací algoritmy SHA a MD jsou navrženy pro rychlý výpočet otisku velkých vstupních souborů, takže i generování tabulek pro různé kombinace znaků bude rychlé. Doporučuje proto použití algoritmu bcrypt, což je adaptivní hashovací algoritmus založený na postupech ze šifrovacího algoritmu Blowfish (viz).

Bcrypt kromě hesla a salt řetězce pracuje ještě s parametrem „cost“, kterým lze ovlivnit náročnost výpočtu hashe. Můžeme tak snadno nastavit algoritmus tak, že výpočet hashe bude trvat třeba sekundu – u přihlášení uživatele ani při registraci to nepředstavuje větší problém, ovšem pro výpočet „rainbow tables“ je třeba tisícinásobně pomalejší algoritmus výrazným faktorem. Počítat tabulky týden je únosné, ovšem počítat je devatenáct let nebude asi nikdo.

Více k tématu viz: py-bcrypt, Modern password hashing, How to use bcrypt in PHP, Please use bcrypt to hash your passwords, Bcrypt implementation in JavaScript

Na časování záleží!

Když porovnáváme hash uživatelem zadaného hesla s tím, jaký je uložen v databázi, měli bychom používat „časově ekvivalentní porovnání“. Důvody popisuje již zmíněný Coda Hale v článku A lesson in timing attacks. Tyto útoky využívají faktu, že porovnávání řetězců většinou končí při nalezení první neshody, takže při dostatečně přesném měření odezvy lze zjistit, která hodnota byla vyhodnocována delší dobu a iterativně tak dojít k hodnotě celého řetězce.

Autentizace

Uživatele máme přihlášeného, co s ním dál? Asi nejčastější postup je vytvoření „session“, sezení, jehož identifikátor je uložen do cookie, a ten, kdo se touto cookie prokáže, je považován za ověřeného uživatele. Slabé místo je „session stealing“, neboli využití otevřené session útočníkem. Servery proto kontrolují např. IP adresu a při její nápadné změně sezení ukončí a uživatele odhlásí.

Pokud je v aplikaci „díra“, může útočník využít např. XSS a volat API serveru se stejnými oprávněními jako přihlášený uživatel – k dotazu jsou přidány cookies s informacemi o otevřeném sezení jaksi „automaticky“.

A co API?

Moderní webové aplikace se posouvají ze serverů na klienty – zatímco ještě před pár lety bylo naprosto přirozené, že prohlížeč sloužil jen jako zobrazovací zařízení pro „interface“, který byl kompletně generovaný a vyhodnocovaný na straně serveru, dnešní AJAXové aplikace obsahují už velkou část logiky v JavaScriptu, na straně prohlížeče, a se serverem často komunikují přes API.

V dnešních webových aplikacích je navíc stále běžnější, že HTML není jediným výstupem webového serveru. Informace bývají prezentovány čím dál častěji prostřednictvím různých API v podobě JSON či XML, a klientským programem, který se k webovému serveru připojuje, už dávno není výhradně prohlížeč. Ruku v ruce s tím jde i odklon od běžného modelu „přihlášení – probíhá sezení, udržované přes cookie a SessionID – odhlášení“, který bývá, zejména u volání API, nahrazován „per request“ autentizací, kdy klient např. „podepisuje“ každý požadavek nebo požádá o jednorázový (či krátkodobý) přístupový token, který funguje pro jeden požadavek (nebo malou sérii požadavků) a pak je zneplatněn.

Na principu podepisování požadavků funguje i HTTP autentizace, která má ale pro AJAXové aplikace jednu zásadní nevýhodu: nelze ji zatím „ovládat skriptem“. Nelze poslat požadavek a určit jméno a heslo skriptem, vždy „vyběhne“ standardní přihlašovací dialog. (Poznámka – neuvažujeme „basic“ autentizaci, která posílá jméno a heslo v otevřeném textu; o takové autentizaci ani neuvažujme jako o bezpečnostním mechanismu!)

Pokud server vrací 401 Unauthorized, zobrazí prohlížeč dialog (nebo použije uložené heslo pro dané sezení), což může být chování nežádoucí z mnoha důvodů, a tím nejprostším je třeba to, že chceme mít přihlašovací formulář jako součást stránek, v našem designu, či místo „jméno“ do dialogu napsat „mail“.

Vsuvka: znovuvynalézáme HTTP Digest

Když jsem před časem nad přihlašováním v AJAXových aplikacích přemýšlel, říkal jsem si: Vždyť přece není nutné posílat na server jméno a heslo! Obojí může zůstat uložené v prohlížeči, jméno v localstorage, heslo v sessionstorage, a vůči serveru se lze ověřit např. pomocí postupu „výzva – odezva“. Při ověřování myšlenky jsem záhy zjistil, že „vynalézám kolo“ – lépe řečeno že jsem „vynalezl“ HTTP Digest Authentication: klient poslal požadavek, server vrátil výzvu (nonce), na klientu se z nonce, jména a hesla spočítal hash a požadavek byl poslán znovu, spolu s tímto hashem.

Napsal jsem tedy implementaci HTTP Digest autentizace pro AJAXová volání v JavaScriptu, ovšem narazil jsem na výše uvedené: odpověď serveru se stavovým kódem 401 znamenala otevření dialogu prohlížeče a autentizaci „mimo možnosti JavaScriptu“. Jako dostupné řešení se nabízelo použití jiného 4XX kódu, který dialog nevyvolá. Server pak podle příznaku určil, jestli přistupuje AJAXové knihovna nebo běžný prohlížeč, a podle toho poslal buď 401 nebo „speciální 4XX“ status. Výsledek byl funkční, ale koncepčně to je poněkud na hlavu padlé – čti: hack.

Je posílání jména a hesla při loginu přírodní zákon?

Přesto by to byla možná cesta, podobná té, kterou používá např. PayPal API. Jméno a heslo neputuje po síti, zůstává na klientské straně (v prohlížeči). Můžeme ho uložit do zmíněné SessionStorage, klidně jako předvypočítaný hash, např. jako f("sessid" + f("login:password")), kde f() je hashovací funkce a sessid identifikátor sezení. Server má v databázi hodnotu f("login:password"), identifikátor sezení se nějakým způsobem dozví (pro paranoiky třeba Diffie-Hellman algoritmem) a může si tak spočítat stejný hash. Tuto hodnotu použije klient pro „podepsání“ požadavků a server pro „ověření podpisu“. Přitom podpis nemusí být jen „hash zpávy + ověřovacího řetězce“ – můžeme použít třeba šifrování AES, na klientu celý požadavek převést např. na JSON reprezentaci, zašifrovat via AES, kde klíčem bude výše zmíněný předvypočítaný hash, a na serveru opět rozšifrovat.

Nastíněný postup připomíná SSL, čímž se dostáváme k použití HTTPS. Pravděpodobně nic nezkazíte tím, když jej použijete tam, kde to je možné. Pozor ale na míchání „HTTPS“ a „non-HTTPS“ objektů na jedné stránce!

Dokonce nemusí ani žádné sezení vzniknout a klientská aplikace (klidně napsaná v HTML/JS) může přistupovat k serveru čistě přes jednorázově podepsané API požadavky (či malé série). Bude tak postavena na stejnou úroveň jako třeba aplikace ve Flexu, nativní aplikace pro mobilní platformy či jiný „neprohlížečový“ klient, což zase posílí rozdělení aplikace na klientskou a serverovou část, které je dnes mnohdy nejasné…

Na místě je ale podotknout, že díra v aplikaci (XSS) znamená okamžitě „nulovou ochranu“ – útočník může v takovém případě napadnout cokoli, od formulářů přes úložiště až po algoritmy, a celá ochrana pak bude naprosto zbytečná, i kdybychom používali silné šifry, HTTPS a solili hesla Buchtingovou solí.

Řekni jméno, nebo…!

Pojďme ještě dál – je vůbec nutné ukládat na serveru jméno a heslo? Měl by tam být nějaký „uživatelský identifikátor“, ale nemusí to být přeci vůbec tato „svatá dvojice“! Místo jména může být na serveru také klidně „bezrozměrný hash“ – kolizi jmen při registraci odhalí, jednoznačnou identifikaci umožní a jinde „jméno“ zapotřebí není, ba naopak – čím míň toho bude server o uživatelích „vědět“, tím nižší pravděpodobnost, že případné odcizení databáze uživatelů povede k jejich kompromitaci na jiných webech. Identifikátor pro uživatele navíc klidně může spočítat samotný prohlížeč ze zadaného jména.

Mnoho z těchto postupů lze nalézt v nových autentizačních metodách (bezrozměrný ID uživatele v LiveID, Diffie-Hellman algoritmus v OAuth). Mnoho inspirace lze nalézt i ve starých osvědčených metodách (Kerberos). Zkrátka – ukládání „jména a hashe hesla“ do databáze na serveru, používané často jako „možnost první volby“, nemusí být dnes jedinou použitelnou možností. Stejně tak „session“ – nové prohlížeče, které podporují WebStorages, mohou používat jiné formy autentizace, bližší postupům z API a méně náchylným např. k „odcizení session“.

Samozřejmě je vždy potřeba zvážit, zda nová technologie nepřinese nová, zatím neznámá rizika, a zda bude dostupná pro dostatečný počet uživatelů.

tl;dr

  • Nepoužívejte pro ukládání hesel prostý text
  • Nepoužívejte pro hesla jednoduché hashovací funkce (MD5, SHA1); použijte bcrypt
  • Model „přihlášení + session v cookie“ není jediným možným modelem; zkuste využít moderní techniky
  • HTTPS pomůže
  • Tomu, kdo má v aplikaci XSS, nepomůže nic!

Začal programovat v roce 1984 s programovatelnou kalkulačkou. Pokračoval k BASICu, assembleru Z80, Forthu, Pascalu, Céčku, dalším assemblerům, před časem v PHP a teď by rád neprogramoval a radši se věnoval starým počítačům.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 44

Přehled komentářů

KapitánRUM ...no...
František Kučera Basic autentizace vs. CRAM
Martin Malý Re: Basic autentizace vs. CRAM
Lokutus Re: Basic autentizace vs. CRAM
kutilm Moderní techniky
pepazdepa jak jsou na tom zname hvezdy?
Jakub Vrána Re: jak jsou na tom zname hvezdy?
janpoboril Re: jak jsou na tom zname hvezdy?
František Kučera Re: jak jsou na tom zname hvezdy?
tuttle Výborně!
seberm bcrypt v php?
patrik.sima Re: bcrypt v php?
Martin Malý Re: bcrypt v php?
shade Re: bcrypt v php?
patrik.sima Re: Několik poznámek k heslům
turista Re: Několik poznámek k heslům
MAge Re: Několik poznámek k heslům
kutilm Re: Několik poznámek k heslům
kutilm Re: Několik poznámek k heslům
František Kučera Re: Několik poznámek k heslům
Karel Re: Několik poznámek k heslům
kutilm Re: Několik poznámek k heslům
Jakub Vrána Re: Několik poznámek k heslům
kutilm Re: Několik poznámek k heslům
Jakub Vrána Re: Několik poznámek k heslům
FHonza Re: Několik poznámek k heslům
kutilm Re: Několik poznámek k heslům
nikdo Timing attack
Martin Malý Re: Timing attack
kutilm Re: Timing attack
kutilm Re: Timing attack
František Kučera Re: Timing attack
kutilm Re: Timing attack
Karel Re: Timing attack
biggringo Re: Timing attack
petík Re: Timing attack
kutilm Důležitost chráněných dat
nikdo Re: Důležitost chráněných dat
Martin Malý Re: Důležitost chráněných dat
kutilm Re: Důležitost chráněných dat
Martin Malý Re: Důležitost chráněných dat
kutilm Re: Důležitost chráněných dat
Jiří Kosek Re: Důležitost chráněných dat
kutilm Re: Důležitost chráněných dat
Zdroj: https://www.zdrojak.cz/?p=3519