Znów o hasłach, tym razem bardziej pod kątem implementacji mechanizmów uwierzytelniania w aplikacji (głównie w aplikacji internetowej).
O implementowaniu haseł
Zacznijmy może od prostego schematu:

Na schemacie tym przedstawione są następujące elementy:
- klient, który dla aplikacji jest w zasadzie bytem zewnętrznym,
- przepływ danych między klientem i aplikacją oraz aplikacją i klientem,
- Trust Boundary między klientem i aplikacją,
- aplikacja,
- składnica danych zawierająca dane użytkowników (dane uwierzytelniające),
- przepływ danych między aplikacją a bazą danych użytkowników (w obie strony),
- składnica danych zawierających logi,
- przepływ danych między aplikacją i logami (w jedną stronę),
Historia, którą przedstawia ten schemat jest następująca (dość typowa sytuacja):
- klient chce uwierzytelnić się w aplikacji, wysyła do aplikacji swoje dane uwierzytelniające,
- aplikacja dokonuje sprawdzenia danych uwierzytelniających w klienta w swojej bazie,
- do klienta zwracana jest informacja o powodzeniu lub niepowodzeniu operacji,
- aplikacja prowadzi log, w którym zapisuje informacje o swoim działaniu i akcjach użytkowników,
Na początek zajmijmy się tym, co jest poza (lub częściowo poza) Trust Boundary. Jest to zewnętrzny dla systemu klient oraz przepływ danych między klientem i aplikacją.
Jak już wspomniałem klient jest w zasadzie dla systemu bytem zewnętrznym (external entity). Z tym elementem wiążą się (według STRIDE) spoofing oraz repudiation, którym przeciwdziałamy stosując mechanizm uwierzytelnienia (który właśnie implementujemy) oraz prowadząc logi. Wychodząc nieco poza STRIDE-per-element przy implementacji haseł warto zadbać, by po stronie klienta hasło nie było nigdy zapisywane. Nie mówię tu oczywiście o żółtych karteczkach, ale o zapamiętywaniu wpisanego przez użytkownika hasła przez, niosące pomoc leniwym użytkownikom, przeglądarki. By uniknąć takiego information leak na polach, w które wpisuje się nazwę użytkownika i hasło należy dodać atrybut AUTOCOMPLETE=OFF. Przy okazji ciekawa kwestia: The Problem with Password Masking. Cóż, ja jestem staromodny i uważam, że hasło jednak nie powinno być wyświetlane w trakcie wpisywania. Ostatnią rzeczą, którą trzeba uwzględnić "po stronie klienta" to fakt, że jeśli użytkownik ma malware, to jego hasło może zostać przechwycone, niezależnie od stosowanych "zabezpieczeń" (np. klawiaturek wirtualnych).
Bardziej interesującym elementem jest data flow między klientem i aplikacją. Tutaj głównym celem intruza może być podsłuchanie komunikacji między klientem i serwerem w celu przechwycenia hasła. Dlatego komunikacja ta powinna być szyfrowana, najlepiej cała komunikacja z wykorzystaniem protokołu SSL. Tu można wprost się odwołać do punktu Cleartext Transmission of Sensitive Information z listy 2009 CWE/SANS Top 25 Most Dangerous Programming Errors. Ważne jest, by w sposób bezpieczny przesyłane były nie tylko dane wpisane przez użytkownika, ale również sama formatka logowania przy przesłaniu jej od aplikacji do klienta. Z elementem data flow w STRIDE-per-element związany jest również tampering, co w tym przypadku można zobrazować jako możliwość modyfikacji przez atakującego przesyłanych z aplikacji do klienta danych w taki sposób, by dane uwierzytelniające klienta zostały przesłane bez szyfrowania lub zostały przesłane na całkiem inny adres. Zresztą uwzględniając information disclosure oraz tampering właściwie cała komunikacja między aplikacją a klientem powinna być szyfrowana, co zresztą aktualnie rozważa/testuje Google: HTTPS security for web applications.
Kolejnym elementem tym razem już "po naszej stronie miedzy", jest aplikacja. Tutaj znów trzeba skupić się na information disclosure. Można oczywiście rozważać również inne scenariusze, takie jak na przykład odczytywanie i analiza pamięci procesu czy wręcz modyfikacja samej aplikacji, ale możliwość ich zrealizowania oznacza, że atakujący kontroluje środowisko, w którym aplikacja działa, przed czym dość ciężko się bronić. Problem w tym, że atakującym może być na przykład administrator firmy hostingowej, z której usług korzystamy, ale to już temat na zupełnie oddzielne rozważania.
Intruz atakujący "z zewnątrz" nie powinien uzyskać od aplikacji innej informacji, niż prosta odpowiedź TAK/NIE na "pytanie", czy hasło jest poprawne. Oczywiście wcześniej musi znać odpowiednią nazwę użytkownika, w której ustaleniu aplikacja również nie powinna pomagać. Łamanie haseł "przez sieć" polega zwykle na wysyłaniu do aplikacji kolejnych kombinacji nazwa użytkownika/hasło i sprawdzaniu, czy próba logowania zakończyła się powodzeniem. Ile prób musi wykonać atakujący w celu znalezienia hasła zależy w zasadzie wprost od ilości możliwych haseł, a właściwie od przestrzeni, którą musi przeszukać, o czym pisałem poprzednio.
Wprowadzenie polityki haseł ma na celu wyeliminowanie haseł prostych, słownikowych. Nadal jednak będą możliwe hasła typu Dodatek12 (długość ponad 8 znaków, duża litera, mała litera, cyfra), a wygenerować tego typu hasła na podstawie listy haseł potrafi obecnie już większość narzędzi, choćby wielokrotnie wspominany Cain & Abel. Sprawdzenie haseł słownikowych modyfikowanych w ten sposób nie gwarantuje oczywiście sukcesu, ale w przypadku dużej ilości użytkowników można osiągnąć jakiś sukces, a ilość haseł, które trzeba sprawdzić, jest zdecydowanie mniejsza, niż teoretycznie wyliczona cała "minimalna przestrzeń możliwych haseł". Warto jest w trakcie tworzenia aplikacji uwzględnić mechanizm, który spowoduje, że atakujący nie będzie mógł sprawdzać haseł masowo.
Pierwszym mechanizmem, który w założeniu ma utrudniać (uniemożliwiać) łamanie haseł, jest blokowanie konta użytkownika po kilku nieudanych próbach uwierzytelnienia. Koncepcja ta jest znana od lat i w wielu przypadkach doskonale się sprawdza, ale nie koniecznie w przypadku aplikacji internetowych. O ile rzeczywiście skutecznie może utrudnić łamanie haseł, to jednocześnie w połączeniu z możliwością odgadnięcia/ustalenia nazw użytkowników mechanizm taki wprowadza podatność na atak typu denial of service. Intruz może blokować konta użytkowników uniemożliwiając im skorzystanie z systemu. Atak taki może być bardzo uciążliwy i dla użytkowników, i dla obsługi systemu, zwłaszcza, jeśli procedura odblokowania konta użytkownika wymaga ich udziału. Jeśli z kolei reset hasła jest realizowany samodzielnie przez użytkownika, trzeba ten mechanizm przeanalizować, gdyż może okazać się, że atakujący zamiast łamać hasło, może użyć (nadużyć) mechanizmu resetu hasła, czego przykłady mieliśmy nawet w trakcie ostatniej kampanii prezydenckiej w USA (polecam prześledzenie historii hasła e-mail Sary Palin).
Zamiast stosować mechanizm blokowania haseł, można atakującemu utrudnić zadanie w nieco inny sposób - ograniczając liczbę haseł, które może sprawdzić w jednostce czasu. Można wprowadzić proste opóźnienie oparte o ilość nieudanych prób uwierzytelnienia, na przykład opóźnienie wyrażone w sekundach wyliczane w następujący sposób 2^0 (brak nieudanych prób uwierzytelnienia), 2^1 (jedna nieudana próba), 2^2, 2^3 i... tyle. 2^3 to już 8 sekund, trzecia nieudana próba uwierzytelnienia. W tym miejscu dalsze wydłużanie czasu odpowiedzi może nie być najlepszym pomysłem, zamiast tego można wprowadzić wymaganie podanie CAPTCHA.
Wprowadzenie takiego mechanizmu opóźnień spowoduje, że cztery hasła atakujący może sprawdzić w ciągu 1+2+4+8=15 sekund, po tych czterech hasłach każdorazowo musi udowodnić, że jest człowiekiem co wymusza CAPTCHA. Oczywiście CAPTCHA nie jest mechanizmem doskonałym, irytuje użytkowników (dlatego w tym przykładzie jest wymagana dopiero przy piątej próbie), można ją łamać. Powoduje jednak, że ilość haseł, które można sprawdzać w jednostce czasu znacznie spada. Koszty, które atakujący musi ponieść mogą być wyższe, niż oczekiwany zysk, więc mimo, że teoretycznie istnieją techniczne możliwości (nawet jeśli "techniczną możliwością" będzie zatrudnienie 1000 nisko płatnych "specjalistów od przepisywania tekstów z obrazków"), będzie szukał łatwiejszego celu.
Nie można polegać wyłącznie na technologii. Umieszczenie w systemie informacji o datach ostatniego udanego logowania i ostatniego nieudanego logowania nie jest zadaniem wyjątkowo wymagającym, a pozwoli użytkownikom (co nie znaczy, że z tej możliwości skorzystają) na odpowiednie zareagowanie w sytuacji, w której stwierdzą, że ktoś inny logował się, bądź próbuje logować się na ich konto. Dodanie specjalnej informacji w przypadku stwierdzenia większej ilości prób nieudanego uwierzytelnienia również może tu pomóc.
Podsumowując tą część:
- nie pozwalaj przeglądarkom na zapamiętanie nazwy użytkownika i hasła,
- szyfruj komunikację,
- uważaj z blokowaniem konta, może to zostać wykorzystane do ataku DoS,
- niech kolejne próby uwierzytelnienia zajmują coraz więcej czasu,
- rozważ zastosowanie mechanizmu CAPTCHA by sprawdzić, czy logujący się jest człowiekiem (po kilku nieudanych próbach),
- informuj użytkownika o nieudanych próbach logowania, niech sprawdzi, czy rzeczywiście to on próbował się logować,
To oczywiście nie wszystko, ale coś trzeba zostawić na kolejny odcinek.
- atakujący może mieć w nosie ciasteczka,
- może randomizować User-Agent i wszystkie inne nagłówki
- a ze względu na NAT-y nie bardzo możemy pozwolić sobię na robienie tego per-IP (biorąc pod uwagę popularne aplikacje internetowe to z jednego adresu źródłowego mamy całkiem sporo logowań na minutę).
Jeśli chodzi natomiast o "odsiewanie DoS", to podstawowym problemem może być zidentyfikowanie tego, co jest częścią ataku DoS, a co normalnym działaniem użytkownika. W szczególności może zdarzyć się tak, że odpowiednio "zirytowany" mechanizm "odsiewania DoS" może sam swą nadgorliwością zablokować dostęp prawdziwym, potulnym użytkownikom...
Pokusiłbym się ewentualnie o monitorowanie/wykrywanie anomalii (w tym wypadku ilość prób logowania do systemu w jednostce czasu) i reakcję na wystąpienie anomalii tego typu - np. wymaganie CAPTCHA przy każdej próbie uwierzytelnienia.