Identyfikatory globalne są (ciągle) ZŁE!

W temacie identyfikatorów globalnych wypowiadałem się między innymi tutaj. Temat jednak jest na tyle istotny, że napiszę na jego temat kolejny raz.

Zacznijmy od bazy danych

Poznając temat baz danych szybko pojawia się temat kluczy, w szczególności tak zwanych primary keys. Klucze takie mają (między innymi) dwie miłe cechy:

Wszystko oczywiście z założeniem, że klucz primary jest jednocześnie kluczem unique , co zresztą zwykle jest prawdą. Co więcej klucze te są zwykle monotonicznie rosnące i (w miarę) ciągłe, co też się przydaje, tylko czasami do trochę innych celów, niż było to planowane.

Idąc w górę...

Wspomniane klucze stają się naturalnymi kandydatami na identyfikatory w aplikacjach internetowych. Identyfikatory te mówią na jakim rekordzie wykonana ma zostać określona operacja. A operacja ta to może być na przykład wyświetlenie, usunięcie, zapisanie lub rozpoczęcie modyfikacji danych zawartych we wskazanym rekordzie. W rezultacie bardzo często w adresach spotyka się następujące konstrukcje: show.php?id=1421 , gdzie id jest identyfikatorem rekordu w bazie, który ma zostać wyświetlony. Tym rekordem może być na przykład artykuł na jakimś portalu. Gdy widzę taką konstrukcję, zwykle sprawdzam, co stanie się, gdy zmodyfikuję ją do postaci show.php?id=1421 AND 1=1 — oraz show.php?id=1421 AND 1=0 —. To oczywiście przymiarka do Blind SQL Injection. Ale akurat tym razem nie chodzi mi o SQL Injection , więc tego kierunku nie będę kontynuował.

Jeśli na danym portalu każdy użytkownik może przeczytać każdy artykuł, to modyfikacja parametru id nie daje intruzowi nic. Uzyskuje dostęp do informacji, które i tak są dla niego dostępne. Problem pojawia się wówczas, gdy tak być nie powinno, czyli gdy powinna istnieć jakaś kontrola dostępu.

Przykład pierwszy – Nasza Klasa

Nie tak dawno było głośno o możliwości pobrania z Naszej Klasy danych użytkowników. Tutaj nie było obejścia mechanizmów chroniących dostęp do danych. Problemem był (i jest) link, za pomocą którego można dostać się do danych profilu użytkownika. Wygląda on mniej więcej tak: http://nasza-klasa.pl/profile/XXXXXX gdzie XXXXXX to oczywiście nic innego jak identyfikator użytkownika. Oczywiście globalny i monotoniczny. Napisanie skryptu, który przejdzie przez sekwencję identyfikatorów i pobierze z wyświetlonych stron dane użytkownika, jest trywialne. Zamiast takich identyfikatorów globalnych należałoby raczej użyć coś, co nie jest monotoniczne. W najbardziej trywialnym przypadku może to być jakiś HMAC. W tym przypadku, gdy identyfikator miałby postać eb65d041f9a42b97b62c051e5d7e205a przejście sekwencyjne przez wszystkich zarejestrowanych użytkowników nie byłoby już tak trywialne. Można myśleć o jakichś dodatkowych zabezpieczeniach jak na przykład CAPTCHA, która byłaby wymagana po przekroczeniu pewnego limitu odwołań do strony z danymi profilu.

Powyższy przykład to dopiero rozgrzewka, na dobrą sprawę i tak wszystkie dane dostępne w NK są dostępne dla zarejestrowanych użytkowników, nie chodzi więc w tym przykładzie o kontrolę dostępu.

Przykład drugi – Nasza Klasa

Przykładem, w którym kontrola dostępu już jest wymagana, jest wiadomość w NK. Akurat zostałem przy NK, bo jest to dobry przykład złego przykładu. Na przykład w celu odczytania wiadomości należy odwiedzić stronę http://nasza-klasa.pl/poczta/XXXXXXXX , gdzie XXXXXXXX ponownie jest identyfikatorem wiadomości. Co więcej jako odbiorca wiadomości mam dodatkowo możliwość:

Zajmę się tylko tematem usunięcia wiadomości. I od razu zaznaczam, że to jest PRZYKŁAD i wcale nie znaczy, że akurat w NK jest błąd implementacji, o którym piszę.

Do usunięcia wiadomości należy podać identyfikator wiadomości, która ma być usunięta (lub zapisana). Tak więc do serwera idzie informacja o:

Po stronie serwera natomiast jest konieczne zweryfikowanie, czy konkretny użytkownik ma prawo do wykonania akcji na danym elemencie. I właśnie tutaj jest często problem...

Jakie błędy są popełniane

Mogę dać przykład kilku typowych:

Oczywiście nie zawsze zastosowanie identyfikatora globalnego spowoduje tego typu problemy. Programista może być konsekwentny i dla każdego przykładu może zaimplementować odpowiedni mechanizm sprawdzający czy obiekt, na którym akcja ma być wykonana (bo identyfikatory globalne odnoszą się przede wszystkim do kontroli dostępu do danych) pozwala na wykonanie takiej akcji przez danego użytkownika (w szczególności – czy do niego należy). Ta dowolność jednak przypomina mi słynne “zakazane” funkcje służące na przykład do kopiowania stringów (przykład takiej listy). Programista też mógł sprawdzić, czy bufor jest na tyle duży, by kopiowane dane się w nim zmieściły. Jednak programiści nie robili tego na tyle często, że wprowadzono zamienniki...

Likwidacja identyfikatorów globalnych

Zamiennikiem dla identyfikatorów globalnych są identyfikatory lokalne. Oznacza to, że na przykład każda z wiadomości w mojej skrzynce pocztowej indeksowana będzie numerami od 1 do n. Podobnie jak użytkownika X, Y i Z. Nie ważne jak będę modyfikował identyfikatory, trafię albo w swoją wiadomość, albo w pustkę (jeśli nie będzie można zmapować przekazanego identyfikatora na rzeczywisty identyfikator rekordu w bazie danych).

Może się wydawać, że wprowadzenie takiego rozwiązania jest bardzo złożone i pracochłonne. Cóż, w aplikacjach, które z niego korzystały, NIGDY nie udało mi się dostać do cudzych danych. W aplikacjach, które korzystają z identyfikatorów globalnych (rzeczywistych identyfikatorów z bazy danych) tego typu błędy są bardzo częste... Dlatego też coraz więcej frameworków udostępnia mechanizmy, które upraszczają ukrywanie globalnych (rzeczywistych) identyfikatorów przed użytkownikiem.

Oryginał tego wpisu dostępny jest pod adresem Identyfikatory globalne są (ciągle) ZŁE!

Autor: Paweł Goleń