Hlavní navigace

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

Aktualizováno: Aktualizováno (13. 4. 2006 0:00)
Jakub Vrána 12. 4. 2006

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
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)));
}
?> 

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, 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;
$login = (get_magic_quotes_gpc() ? $_POST["login"] : addslashes($_POST["login"]));
$row = mysql_fetch_assoc(mysql_query("SELECT password_md5 FROM users WHERE login = '$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)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é.

Našli jste v článku chybu?

12. 4. 2006 10:45

A co takhle?

  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.

12. 4. 2006 0:26

peta (neregistrovaný)
Ahoj, článek je OK, jen v něm myslím chybí zmínka o tom, že v případě použití challenge-response mechanismu je třeba zvláštní péči věnovat ochraně databáze, kde jsou uloženy otisky hesel. Při zcizení otisků se může zručnější útočník přihlásit v podstatě jako jakýkoli uživatel - stačí jen trochu poupravit uvedený JavaScript, aby již otisk nehashoval. Bohužel ani C-R tedy není "100% bezpečný"...
Podnikatel.cz: Platební brány a EET? Stále s otazníkem

Platební brány a EET? Stále s otazníkem

Vitalia.cz: Vláknina: Rozpustná, nebo nerozpustná?

Vláknina: Rozpustná, nebo nerozpustná?

Lupa.cz: E-shopy: jen sleva už nestačí

E-shopy: jen sleva už nestačí

Vitalia.cz: Jak koupit Mikuláše a nenaletět

Jak koupit Mikuláše a nenaletět

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: Tesco: Chudá rodina si koupí levné polské kuře

Tesco: Chudá rodina si koupí levné polské kuře

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Vitalia.cz: Žloutenka v Brně: Nakaženo bylo 400 lidí

Žloutenka v Brně: Nakaženo bylo 400 lidí

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Měšec.cz: Finančním poradcům hrozí vracení provizí

Finančním poradcům hrozí vracení provizí

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Vitalia.cz: Baletky propagují zdravotní superpostel

Baletky propagují zdravotní superpostel

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky