Pozor museum: Toto je archivní verze blogu. Pro vkládání komentářů přejděte na tento článek v nové verzi blogu.

Ověřování uživatelů na webu

Tuhle otázku musí řešit čas od času každý tvůrce webu. Kromě hotových řešení vestavěných do CMS nebo lahůdek typu OpenID, Kerberos či LDAP, se můžeme rozhodnout i pro svoji vlastní implementaci. Uživatelská jména a (hlavně) hesla jsou citlivá věc, a proto se budeme zabývat v první řadě bezpečností.

Na co si dát pozor:

  • Přenos hesel mezi uživatelem a serverem v čisté podobě: heslo jde odposlechnout.
  • Uložení hesel v databázi na serveru v otevřeném tvaru: žádný server není nedobytný, útočník může ukrást databázi ze serveru a zkusit použít hesla uživatelů na jiných serverech (hodně uživatelů používá všude stejné heslo). Kromě toho se může dostat k zálohám (pokud zálohujeme :-) a hesla si vydolovat z nich.
  • Ukradení session (relace): útočník může odposlechnout síťový provoz mezi uživatelem a serverem a potom se vydávat za uživatele aniž by znal heslo (to platí v obvyklém případě, kdy se uživatel autentizuje jen na začátku sezení a potom už se pro ověření používá token/SessionID).

Základní pojmy

  • Autentizace: operace, při které zjišťujeme totožnost subjektu. Ptáme se, kdo to je? Odpověď je ten (autentizace).
  • Autorizace: operace, při které zjišťujeme, jestli je subjekt oprávněn k nějaké činnosti, např. přístup k objektu. Ptáme se, co může dělat? Odpověď je to (autorizace). Díky mé mnemotechnické pomůcce si už tyto pojmy nemusíte plést nebo dokonce vytvářet patvary jejich kombinováním a vypadat pak před ostatními jako idioti :-)
  • SessionID: jelikož protokol HTTP je bohužel bezestavový, musíme si pomoci nadstavbou na aplikační úrovni (není to architektonicky nejšťastnější, ale funguje to). SessionID je řetězec nebo číslo, který si klient a server vyměňují během jednotlivých HTTP požadavků a odpovědí (obvykle jako cookie, případně GET parametr). Pomocí tohoto ID si můžeme jednotlivé HTTP požadavky spárovat a propojit si je v relace jednotlivých uživatelů.
  • Hash: Hashovací funkce je typem jednosměrné funkce, která z libovolně dlouhého vstupu (text, posloupnost bajtů) udělá výstup o předem pevně dané délce (hash), který je tvořen uzavřenou množinou prvků (např. malými písmeny a čísly). Výpočet hashe ze zprávy je rychlý, naopak výpočet zprávy z hashe je nehorázně pomalý (resp. tak pomalý, aby byl prakticky nemožný) a kromě toho se původní zprávy nedobereme (zjistíme leda jinou zprávu se stejným hashem). Příkladem hashovacích funkcí jsou MD5, SHA1, SHA256. Více na Wikipedii.

Příklad - Systém BG

Vytvořil jsem jednoduchý systém na stedování chyb/požadavků. Praktické využití nemá v podstatě žádné (je opravdu příliš jednoduchý), ale dobře nám poslouží jako ukázka a inspirace pro řešení bezpečnosti www stránek.

Ověřování uživatelů v aplikaci BG funguje na principu výzva-odpověď (CRAM). Vypadá to zhruba takhle:

  1. Server vygeneruje náhodný řetězec, říkejme mu hesloBordel, a odešle ho jako výzvu uživateli v rámci přihlašovací stránky. Důležité je, že server si tento řetězec pamatuje (má ho spojený s aktuálním sessionID).
  2. Uživatel vyplní do přihlašovacího formuláře jméno a heslo a stiskne tlačítko "Přihlásit se" - stiskem tohoto tlačítka se formulář ještě neodešle (to by se odeslalo heslo v čisté podobě). Stisk tlačítka vyvolá událost, která vypočítá funkci sha1(zadanéHeslo + hesloBordel) a výsledek (hash) této funkce teprve odešle jako heslo na server.
  3. Server od uživatele dostane jméno, hash a sessionID. Podle sessionID si vzpomene na hesloBordel, který nám v prvním bodě poslal. Podle uživatelského jména si dohledá v databázi heslo a v tuto chvíli je schopný porovnat očekávaný hash s hashem, který mu zaslal uživatel. Pokud jsou hashe totožné, uživatel je autentizován a server si k danému sessionID přiřadí jméno uživatele.

Ukládání hesel na serveru v otevřeném tvaru je trestuhodné a zbytečné bezpečnostní riziko, kromě toho nám nepřináší žádný užitek. Místo hesel si tedy do DB uložíme pouze jejich hashe. To nám ale trochu zkomplikuje předchozí postup (bez znalosti hesla, bychom na serveru nebyli schopni vypočítat předpokládaný hash). Řešení je poměrně jednoduché: ve druhém bodě klient místo sha1(zadanéHeslo + hesloBordel) vypočítá sha1(sha1(zadanéHeslo) + hesloBordel) - výsledek funkce sha1(zadanéHeslo) už v DB uložený máme a tudíž nám na serveru nebude dělat problém vypočítat předpokládaný hash pro daného uživatele.

Aplikace BG řeší i možnost ukradení sessionID. Na serveru si u každé sessionID sledujeme IP adresu uživatele. Pokud by se nám s daným sessionID (které odposlechl) pokusil vnutit jiný uživatel, dokážeme to odhalit, protože bude mít jinou IP adresu. Toto řešení má dva nedostatky:

  • Jedna IP adresa nemusí znamenat jeden uživatelský počítač: uživatel může být za proxy nebo NATem a sdílet jednu veřejnou IP adresu s mnoha dalšími uživateli. Takhle to bohužel vypadá často ve firmách. Pokud by došlo k ukradení sessionID jiným uživatelem za téže proxy (NATem, nebo na stejném víceuživatelském počítači), aplikace BG nic nepozná.
  • Uživateli se může měnit jeho IP adresa: například pokud mu spadne síťové spojení (vytáčené, WiFi), nebo pokud používá TOR. V tom případě je od aplikace odhlášen a musí znovu proběhnout jeho autentizace.

Přesto je kontrola IP adresy lepší než nic (v aplikaci BG ji lze případně vypnout).

Aplikaci BG si můžete stáhnout tady: Aplikace BG. Moje zdrojové kódy jsou vydané pod licencí GPL. JavaScriptové knihovny pro výpočet hash funkcí jsou pod licencí BSD. Aplikace je plně funkční v prohlížeči Firefox, je možné, že v jiných prohlížečích budou některé funkce nedostupné.

Další krok k dokonalosti - přisolíme

Aplikace BG má ve své databázi uložen SHA hash hesla. Pokud by se útočník do této databáze vlámal, mohl by hashe použít i u jiných služeb, které pro autentizaci přijímají od uživatelů hash jejich hesla (za předpokladu, že uživatel používá stejné heslo).

Řešení (které ale v aktuální verzi není implementované) se nazývá sůl (salt) a spočívá v tom, že do DB si místo prostého hashe hesla uložíme výsledek funkce hash(heslo + sůl). Jako sůl je vhodné volit řetězec, který bude používat jen naše služba (např. 123frantovo.cz8as7g28nbusr12387fs84g), měli bychom se tedy zdržet oblíbené posloupnosti znaků hovno. Jelikož stejný nápad by mohl mít i správce jiné služby a sůl by tak ztratila na významu.
Také bychom měli použít pro každého uživatele jinou sůl – tím ještě více ztížíme práci útočníkovi, který by si chtěl vygenerovat duhové tabulky pro hesla s naší solí (takhle je bude muset generovat pro každý účet, což se mu nevyplatí). Dalším vylepšením by mělo být, že stejně jako server posílá hesloBordel, měl by i totéž udělat klientský skript (to pro případ, že bychom nekomunikovali s naším serverem, ale s útočníkem, který se za něj vydává, a chce z nás vylákat odpověď pro hesloBordel, které se mu hodí).

Buď a nebo

Ukládání hesel v čistém tvaru (nešifrované, nezahashované) podobě na serveru je špatné ze dvou důvodů - útočník, který se zmocní databáze (případně její zálohy) se může:

  • neoprávněně přihlásit k naší službě
  • neoprávněně se přihlásit k jiné službě za předpokladu, že uživatel používá všude stejná hesla

Hash hesla (i přisolený) má bohužel jednu společnou vlastnost s řešením, kdy v DB máme hesla v otevřeném tvaru: pokud používáme metodu výzva-odpověď (popsaná výše), tak náš uživatel nemusí znát svoje heslo, protože první hash hesla (případně přisolený hash) se počítá ještě na straně klienta. V tom případě útočník, který získá databázi hashů hesel, se může přihlásit k naší službě (ale ne k jiným, protože hashe solíme).

Musíme si tedy vybrat menší zlo, ze dvou možností:

  • Použijeme metodu výzva-odpověď. Tím jsme chráněni proti odposlechnutí hesla protože po síti mezi serverem a klientem neputuje heslo, ale jeho druhý (přisolený) hash (ten je navíc vždy jiný, protože i výzva je vždy jiná - náhodná). Nevýhodou ale je to, že když útočník získá přístup k databázi, může se přihlásit k naší službě (stačí mu k tomu hash hesla, který najde v DB)
  • Klient posílá heslo na server v otevřeném tvaru. Potom můžeme mít na serveru pouze hash a ukradená databáze je tak útočníkovi prakticky k ničemu, protože pomocí hashe se nepřihlásí. Nevýhodou je to, že hesla jdou snadno odposlechnout.

Bez použití asymetrické kryptografie lepší řešení nemáme, je to tedy buď a nebo. Já jsem si vybral první možnost, protože riziko odposlechnutí síťového provozu je vyšší, než riziko, že se nám někdo vláme do databáze na serveru. Navíc, když už se tam vláme, tak má pravděpodobně nad naším serverem moc a tudíž by se ke službě mohl tak jako tak přihlásit. Vhodným doplňkem může být šifrování záloh databáze - ukradenou zálohu pak nepůjde použít pro přihlášení ke službě.

Další možnosti

Kromě docela jednoduché a účinné metody výzva-odpověď, můžeme použít i další způsoby zabezpečení:

  • Metoda výzva-odpověď s asymetrickým šifrováním: Na serveru máme uložený pouze veřejný klíč uživatele. Server pošle klientovi výzvu a ten ji svým soukromým klíčem podepíše a odešle zpět. Server na základě veřejného klíče autentizuje uživatele (ověří podpis). Tato metoda je prakticky neprůstřelná, protože odposlechnutá komunikace je útočníkovi na nic, stejně jako ukradená databáze (zmocní se pouze veřejných klíčů, které k přihlášení nemůže použít). Nevýhodou je ale to, že uživatel s sebou musí nosit soukromý klíč (soubor, případně čipová karta) - nestačí mu tedy heslo, které si může pamatovat.
  • HTTPS (SSL/TLS): toto řešení se používá často, protože je jednoduché (nemusíme nic programovat) a standardní (narozdíl od JavaScriptu, který nemusí fungovat všude). Nevýhodou je to, že SSL nemusí náš webhosting podporovat (a problém s certifikáty u virtualhostingu) a také to, že existují způsoby, jak komunikaci podvrhnout, aniž by si uživatel čehokoli všimnul (žádná varování v prohlížeči), pokud nekontroluje otisk certifikátu a CA. Proto je vhodné HTTPS kombinovat s metodou výzva-odpověď (když ještě k tomu šifrujeme zálohy DB, je riziko relativně nízké).
  • Důvěryhodná třetí strana: jedná se o jinou kategorii řešení: uživatel se ověří vůči třetí straně, od ní získá lístek (ticket) a s ním se teprve přihlásí ke službě. Provozovatel služby má méně starostí (s hesly vůbec nepřijde do styku) a odpovědnost se přesouvá na třetí stranu (přenos hesel po síti a uložení hesel v DB). Příkladem je OpenID a Kerberos.
  • Použití více kanálů a jednorázová hesla (OTP): tyto metody jsou v případě běžných webových aplikací příslovečným kanónem na vrabce - používají je ale některé banky (což je dobře, u nich je to nutné). Příklad použití více kanálů: server pošle uživateli šifrovanou SMS na mobil, uživatel si ji odšifruje a přepíše do prohlížeče. Přiklad jednorázových hesel: uživatel má kartičku se seznamem hesel, při každém přihlášení jedno heslo spotřebuje (a už ho nejde víckrát použít). Pokud kartičku ztratí, nechá si ji zablokovat a dojde si pro novou. Místo kartiček můžeme použít kalkulačky, které v sobě mají jednorázová hesla uložena nebo je generují v závislosti na čase.

Napadají vás jiné možnosti? Napište to do komentářů. Pokud budete apliakci BG používat nebo z ní využijete části kódu (což můžete, vydal jsem ji pod licencí GPL), uděláte mi radost, když mi o tom dáte vědět - třeba hoďte odkaz na vaši aplikaci sem do komentářů.

PřílohaVelikost
bg.tar.gz23.32 KB
Průměr: 4 (5 hlasů)

Taky pěkné čtení. Ale

Taky pěkné čtení. Ale ten "man in middle" mi pořád dává kouř. Jediný co mi z toho vzchází jsou tzv. "pilíře" na kterých to držet. (např. vždy přihlásit, ať už je "ten" či "neten".