Sierpień był dość pechowy dla mBanku. Najpierw testy powiadomień push na produkcji, o których już pisałam. Potem znacznie gorsza wpadka, która mocno podważyła wiarygodność systemu tej firmy. Zamiast szukać winnych zastanówmy się co jako rzemieślnicy systemu możemy wynieść z tej lekcji. Co zawiodło? Jak można było temu zapobiec? Jakie testy zwiększą pewność w podobnych przypadkach?
Co się w ogóle stało?
Ktoś na Facebooku napisał, iż dostał SMSa o tym, iż jego dane zostały zmienione. Po zalogowaniu się do systemu okazało się, iż widnieją tam dane innej osoby. Próba interwencji na infolinii zakończyła się porażką, ponieważ dane klienta nie zostały zweryfikowane poprawnie. Autoryzowane transakcje przez internet też nie były możliwe, ponieważ kody weryfikacyjne przychodziły na inny numer telefonu. Potem odezwała się kolejna taka osoba. I kolejna. Inni pisali, iż widzą co prawda swoje dane, ale historia transakcji na pewno nie jest ich, bo dopiero założyli konto. Okazało się, iż takich osób jest więcej. Bank przyznał, iż choćby 200 osób mogło ucierpieć przez ten błąd systemu. To już nie są śmieszne powiadomienia, to jest poważna dziura w systemie. Całą sprawę opisał m.in. Niebezpiecznik.
Reputacja mBanku dość mocno ucierpiała. Nie dość, iż chodziło o pieniądze, to jeszcze o dane osobowe. Bank na pewno stać na to, żeby oddać ewentualne środki klientom. Łamanie RODO i zasad dotyczących poufności danych swoich klientów to zupełnie inna sprawa. Gorzej mogłoby być tylko w systemach dla ubezpieczeń i ratowania życia. Zespoły projektowe niestety często żyją w oderwaniu od realnych problemów branży systemów które tworzą. Wtedy wytwarzane oprogramowanie staje się tylko zbiorem algorytmów.
Jakie algorytmy zawiodły w mBanku?
Powodów mogło być kilka:
- “Przekręcony” identyfikator.
Skończyły się nieskończone liczby, które miały losowo wyznaczać identyfikator. Mógł też zawieźć algorytm losowości, który jak wiadomo czasem się powtarza. jeżeli nie było sprawdzenia, czy dany identyfikator już istnieje po stronie systemu lub bazy danych, stare konto mogło zostać całkowicie “wchłonięte” przez nowe. Biorąc pod uwagę, iż używanie UUID to w tej chwili standard, raczej wątpię w możliwość takiego scenariusza. - Zmiana algorytmu do nadawania numerów kont.
Nawet nieznaczne zmiany mogły spowodować nadanie tego samego numeru konta innej osobie. jeżeli ten numer nie jest (a zakładam, iż nie jest) głównym identyfikatorem w bazie klientów, to istnieje szansa, iż dwóch klientów dostanie ten sam numer konta. Wtedy wchłonięcie konta mogłoby nastąpić dopiero na poziomie składania odpowiedzi z serwera lub choćby na frontendzie aplikacji. Chociaż wtedy nie byłoby problemów na infolinii. - Brak porównywania numerów kont lub dziurawe porównywanie.
Wystarczy, iż metody typu equals i hashCode zostaną niewłaściwie zaimplementowane i całe porównywanie przestaje działać. Numery IBAN mają bardzo wyrafinowaną logikę, bo muszą być unikalne globalnie. mBank nie wyczerpał jeszcze puli swoich kont, zatem o powtarzających się numerach nie powinno być mowy.
Nie ma oficjalnych informacji na temat tego, jaki błąd spowodował problemy. Moim zdaniem prawda leży gdzieś pomiędzy punktem 2 i 3.
Jak można było temu zapobiec?
Na potrzeby tego wpisu przyjmę dość prawdopodobny scenariusz: zawiodło porównywanie numerów IBAN. Widzę kilka obszarów które powinny zostać przetestowane w podobnych aplikacjach.
Testy jednostkowe
W testach jednostkowych skupiłabym się głównie na algorytmach i logice biznesowej. jeżeli sprawdzimy samo porównywanie dwóch numerów kont, to już daje nam spore zabezpieczenie przeciw regresji. Testy oparte na przykładach (czyli te w których wymyślamy dane do testów) można uzupełnić o testy oparte na adekwatnościach. To testy, w których określamy przypadki brzegowe, ale dane do testów są generowane losowo. Tworzenie numerów IBAN ma jasno określone zasady i można je wykorzystać do tworzenia i porównywania kolejnych wartości. Testy oparte na adekwatnościach są trudniejsze w przygotowaniu i wolniejsze, ale dla tak kluczowych elementów logiki biznesowej warto zainwestować ten czas. Więcej na temat tego rodzaju testów znajdziesz w prezentacji Magdy Stożek.
Testy integracyjne
W testach integracyjnych można przetestować cały „flow”, od tworzenia użytkownika, przez zakładanie konta, po wyciąganie danych i wykonywanie transakcji. Wykonywałabym takie testy na poziomie API, ale podobne scenariusze można wykorzystać w testach systemowych z wykorzystaniem GUI, będą one jednak znacznie wolniejsze. Inspirowałam się systemem bankowym, ale podobną logikę można znaleźć w wielu innych aplikacjach.
- Zakładam użytkownika z unikatowym ID oraz innym identyfikatorem (coś na kształt klucza obcego w SQL)
- Zakładam nowego użytkownika z innym ID i tym samym identyfikatorem (powinno zwrócić błąd, np 400 Bad Request)
- Jeśli powyższa operacja się jednak powiedzie, to mogę sprawdzić czy dane zostały nadpisane – który użytkownik posiada ten identyfikator? Czy jest więcej niż jeden użytkownik? Który zostanie zwrócony kiedy posłużę się tym identyfikatorem?
- Gdy chcę przetestować autoryzację to sprawdzam, czy mogą wyciągać dane innej osoby. jeżeli proszę o dane na podstawie identyfikatora, to czy możliwe jest pobranie danych z innym unikatowym ID. Takie zapytanie powinno zwrócić kod 403 Forbidden. jeżeli połączenie kont nastąpiło na poziomie bazy danych, to taki test przejdzie, bo klient jest jeden (nawet jeżeli jest reprezentowany przez różnych użytkowników).
Być może w mBanku takie testy były częścią procesu CI/CD, a zawiodło coś innego. Może się tego dowiemy, a może pozostaną nam domysły. Warto jednak przy każdej takiej wpadce przyjrzeć się swojemu systemowi i jego testom. Czy Twoje testy wykryłyby takie błędy? Czy masz pomysły na inne testy?