Miniatura artykułu

Zanieczyszczenie prototypu w javascript

8 minut

Skopiuj link

Data publikacji: 3/13/2024, 8:52:42

Ostatnia aktualizacja: 5/4/2024

Zanieczyszczenie prototypu (najczęściej spotkasz się z jego angielską nazwą - prototype pollution) to jeden z najpopularniejszych ataków opartych na wstrzykiwaniu kodu (ang. code injection). Dotyczy on wszystkich środowisk, które wykorzystują JavaScript, a jego zrozumienie jest kluczowe, zarówno dla programisty frontend, jak i backend.

Po przeczytaniu artykułu będziesz wiedzieć, na czym on polega, a co najważniejsze, poznasz sposoby na skuteczną ochronę swoich aplikacji.

Prototypy w JavaScript

Zanim wyjaśnię przebieg samego ataku, musimy wiedzieć, jak działają prototypy w JavaScript, w końcu to na nich oparty jest jego mechanizm.

Być może zastanawiałeś się już kiedyś, jakim cudem po stworzeniu pustego obiektu mamy od razu do dyspozycji kilka metod. Odpowiedzią na to pytanie jest prototyp:

W obiekcie person utworzyłem tylko dwie właściwości: name i age. Nadal jednak mogę się odwołać do metod, które pochodzą z prototypu - w tym przypadku jest to toString oraz hasOwnProperty.

Możemy to przedstawić w prosty sposób na diagramie:

Schemat dziedziczenia obiektu person

Schemat dziedziczenia obiektu person

Wynika z tego, że obiekt person dziedziczy po innym obiekcie, znanym jako Object. Jednak tutaj łańcuch się kończy - Object jest jego ostatnim ogniwem, a jego prototypem jest null (co można interpretować jako brak prototypu i zakończenie łańcucha).

Zanim przejdziemy dalej, wspomnę o jeszcze jednej kwestii, która jest niezbędna do zrozumienia działania samego ataku - Object znajduje się na końcu łańcucha wszystkich typów w JavaScript, za wyjątkiem null i undefined, które nie posiadają obiektowego odpowiednika (dla typu number jest to Number, dla string to po prostu String, itd.).

Uproszczony schemat dziedziczenia w JavaScript

Uproszczony schemat dziedziczenia w JavaScript

Oczywiście w JavaScript istnieje znacznie więcej obiektów, które dziedziczą po obiekcie głównym Object. Te, które przedstawiłem na diagramie, to jedynie wierzchołek góry lodowej. Moim celem nie jest jednak pokazanie wszystkich połączeń, a przedstawienie ogólnego działania dziedziczenia w JS.

Prototypy w kodzie

Widzieliśmy już prototypy w teorii. Przyszedł czas na trochę kodu.

Właściwość __proto__ nie widnieje w oficjalnej specyfikacji języka (zamiast tego, powinniśmy używać metody getPrototypeOf, dostępnej w obiekcie głównym Object), jednak jest powszechnie implementowana w przeglądarkach w celu utrzymania kompatybilności wstecznej.

Okazuje się jednak, że przy jej użyciu możemy dodać, lub zmodyfikować istniejące w prototypie właściwości. To właśnie na tym zachowaniu (oraz nieuwadze programisty 😉) opiera się atak, który jest tematem artykułu.

Zanieczyszczenie prototypu

Skoro wiemy, jak działa prototyp, to możemy przejść do sedna tego artykułu, czyli do ataku skierowanego właśnie na tą funkcjonalność języka.

Najbardziej narażony jest kod znajdujący się na serwerze (backend) i to właśnie on pada najczęściej ofiarą złośliwych działań, jednak frontowa część naszej aplikacji także powinna wystrzegać się błędów, które mogą doprowadzić do zanieczyszczenia prototypu.

Jak zatem dochodzi do powstania luki w zabezpieczeniach i do czego może to doprowadzić?

Wyobraź sobie następującą sytuację:
użytkownik wypełnia formularz w aplikacji, w którym podaje swoje imię oraz wiek. Następnie całość jest przesyłana do serwera i zapisywana w bazie danych (dla uproszczenia nie będziemy się skupiać na kodzie związanym z obsługą bazy).

Tak wygląda kod odpowiedzialny za obsługę tego żądania po stronie serwera (celowo pominąłem tu kod związany z routingiem - skupimy się tylko na funkcji przetwarzającej zapytanie).

Na pierwszy rzut oka wszystko wygląda normalnie. Serwer otrzymuje dane od klienta, następnie dodaje do nich dodatkowe wartości: createdAt oraz id.

W tym kodzie istnieje jednak poważna luka bezpieczeństwa. Programista, który go napisał, bezgranicznie ufa, że użytkownik ma dobre zamiary i poprawnie wypełni formularz. Takie założenie jest całkowicie błędne, a dane pochodzące od użytkownika powinny być zawsze weryfikowane.

Zamiast wypełniać formularz dostępny na stronie (zgodnie z intencją programisty), wyślijmy zapytanie za pomocą narzędzia, które to umożliwia. W moim przypadku będzie to Postman.

Oprócz standardowych danych wymaganych w zapytaniu dodałem do ciała także jedną dodatkową właściwość: __proto__. Gdy trafi ono do utworzonego wcześniej endpointu, funkcja łącząca oba obiekty nadpisze łańcuch prototypów, a nowo utworzony obiekt zostanie zainfekowany potencjalnie złośliwym kodem.

Do zanieczyszczenia prototypu może dojść, gdy łączymy ze sobą dwa obiekty, a jeden z nich pochodzi od użytkownika, a więc niezaufanego źródła. Najgorszym z możliwych scenariuszy jest zastosowanie głębokiego połączenia (ang. deep merge) dwóch obiektów, ponieważ może dojść do zanieczyszczenie prototypu obiektu głównego, a wtedy skala ataku może być znacznie większa.

Gdybym ponownie wysłał to samo zapytanie do tak przygotowanego endpointu, to mógłbym spowodować znacznie większe szkody (zwróć uwagę, że funkcja deepMerge rekurencyjnie łączy dwa obiekty).

Co w takim razie może się stać? 🤔

Najmniej dotkliwy w skutkach przypadek to wywołanie błędu po stronie serwera i tymczasowe jego wyłączenie.

Skutki mogą być jednak znacznie bardziej dotkliwe. W najgorszym przypadku może dojść do zdalnego wykonywania kodu, dostępu osób nieuprawnionych do zasobów z bazy danych i wycieku danych użytkowników (ta lista jest znacznie dłuższa).

Dodatkowo, taki atak może pozostać niezauważony przez jakiś czas, jeśli nie spowoduje żadnych błędów, co tylko spotęguje spustoszenia.

Sposoby ochrony

Żeby skutecznie obronić się przed tego typu atakami mamy do dyspozycji kilka różnych możliwości:

  • Oczyszczanie (ang. sanitizing) obiektu w czasie łączenia - zamiast stosować operator ..., który przeniesie wszystkie właściwości, możemy utworzyć funkcję służącą do rekurencyjnego łączenia dwóch (lub więcej) obiektów. Zyskujemy wtedy większą kontrolę nad tym, jakie właściwości trafią do nowo utworzonego obiektu, a co za tym idzie, możemy usunąć niebezpieczne klucze (__proto__, constructor).
    Taka operacja powinna być wykonana zawsze wtedy, gdy mamy do czynienia z danymi pochodzącymi z nieznanego źródła, na przykład od użytkownika.

  • Tworzenie obiektów bez prototypu - w tym celu można wykorzystać Object.create(null, { ... }). Ta metoda ochroni nas przed zanieczyszczeniem obiektu głównego, jednak nie gwarantuje całkowitego bezpieczeństwa.

  • Metoda Object.freeze - zamiast jednak zamrażać każdy obiekt po kolei, możemy zamrozić prototyp obiektu głównego i tym samym zapobiec jego zmianom: Object.freeze(Object.prototype).

  • Zewnętrzne biblioteki - przykładem może być tutaj Lodash, który zawiera metody do bezpiecznego łączenia obiektów.

Podsumowanie

Zanieczyszczenie prototypu to bardzo popularny atak, wymierzony w środowiska wykorzystujące JavaScript. Jego skutki mogą być naprawdę poważne, dlatego podczas pisania kodu należy zwrócić uwagę, czy nie popełniamy błędów, które mogą prowadzić do powstania luk w systemach bezpieczeństwa.

Najczęstszym punktem umożliwiającym atak są dane przesłane przez użytkownika. Nigdy nie powinniśmy traktować ich jako bezpiecznie i zawsze należy je poddać procesowi oczyszczania.

Niestety nie chroni nas to w pełni przed atakiem opisanym w artykule, ponieważ wektorem ataku mogą być również zewnętrzne biblioteki, w których autor popełnił błąd podczas implementacji, narażając naszą aplikację na bezpośrednie niebezpieczeństwo.

Niestety nie jesteśmy w stanie śledzić kodu każdej stosowanej biblioteki - to co możemy zrobić, to częste ich aktualizacje (być może w nowszej wersji podatność została usunięta) oraz stosowanie systemów automatycznego wykrywania podatności na różnego rodzaju ataki. Za przykład może tu posłużyć Snyk lub Dependabot, stworzony przez Github.

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy