Bezpieczeństwo aplikacji internetowych i najlepsze praktyki bezpieczeństwa w Pythonie

Luty 27, 2019 Michał Wodyński

Jakiś czas temu na spotkaniu Wroc.Py poruszyłem temat bezpieczeństwa w programowaniu. Temat jest niezwykle ważny, a niestety potrafi często umknąć w nawale obowiązków i ciśnienia deadlineów. Wiadomo, że dostarczenie produktu na czas jest istotne, ale nie kosztem bezpieczeństwa, które wpływa m.in. na wiarygodność naszego biznesu. Większość problemów z bezpieczeństwem wynika z braku znajomości działania ataków, ale także winne jest używanie niezabezpieczonych bibliotek lub funkcji z modułów, które są podatne na ataki.

Bezpieczeństwem interesowałem się już na studiach. Zacząłem od poszczególnych zabezpieczeń we frameworku Django, przeszedłem na tematy bezpieczeństwa kart RFID oraz na temat kryptografii w czujnikach bezprzewodowych. Od czasów studiów programuję w Pythonie a refleksja na temat własnego kodu była przyczyną do stworzenia tej prezentacji. Wiele linijek mojego kodu trafiło na środowisko produkcyjne a ja nie miałem jednak pojęcia, czy było faktycznie odporne na wektory ataków. W związku z tym zapisałem się na kurs pt: „Atakowanie i ochrona aplikacji webowych” z Niebezpiecznika i postanowiłem dokładnie poznać temat. Kurs był bardzo obszerny, zastanawiałem się jak zebrać w pigułce niezbędną widzę na temat bezpiecznego programowania w Pythonie. Gdy zgłębiłem temat bezpieczeństwa na własną rękę. Okazało się, że ilość niebezpiecznych miejsc w Pythonie mnie zaskoczyła.

Czas na przykłady. Obszernym tematem bezpieczeństwa jest problem parsowania plików XML. Język XML jest to język znaczników, który powstał w 1996 roku. Większość dziur w bezpieczeństwie jest znana od dawna, np: atak Billion Laughs Attack został po raz pierwszy zaraportowany w 2003 roku. Pomimo to, niektóre z bibliotek XML wciąż są podatne na ataki i nawet ich użytkownicy są bardzo często zaskoczeni tym faktem. Ciężko jest winić za to twórców bibliotek zważywszy, że przy implementacji podążali za specyfikacją XML. Deweloperzy, którzy rozwijają aplikacje powinni po prostu wiedzieć, że biblioteki których używają nie są dobrze napisane pod względem bezpieczeństwa i mogą wyrządzić potencjalną szkodę. Rezultaty ataków na podatne biblioteki XML mogą być dramatyczne. Kilkaset bajtów pliku XML może sprawić, że kilka GB pamięci zostanie wykorzystane w przeciągu kilku sekund. Innym objawem może być utrzymywanie procesora przez dłuższy czas przy użyciu nawet małych porcji plików XML. W szczególnych przypadkach może się okazać, że osoba atakująca ma dostęp do lokalnych plików na serwerze omijając firewalla lub po prostu używa zasobów do odbycia ataku na inne zasoby osób trzecich. Ataki wykorzystują mniej znane feature’y pików XML i ich parserów. Większość deweloperów nie jest świadoma zagrożeń wynikających z instrukcji przetwarzania albo z Expansion Entity, które zostało odziedziczone po SGML. W najlepszy przypadku są świadomi znacznika. Natomiast nie wiedzą, że definicja typu dokumentu (DTD) może wygenerować zapytanie HTTP albo załadować plik z lokalnego dysku. Poniżej znajduje się tabela w której pokazane jest zastawienie podatności poszczególnych bibliotek Pythonowych na ataki.

Dużym problemem jest również używanie innych bibliotek, które posiadają różnego rodzaju podatności. Chodzi między innymi o używanie nieaktualnych wersji bibliotek, czy zapominanie o zależnościach zachodzących pomiędzy nimi… same biblioteki mogą być podatne na ataki, na przykład gdy używamy pipa kolejność instalowania bibliotek może spowodować obniżenie wersji jednej z nich. Pipenv zachowuje się nieco lepiej ze względu na to, że przed instalacją rozwiązuje problemy z zależnościami, ale… efekt końcowy jest ten sam. Dlatego jest istotne, by używać projektów które są wspierane i utrzymywane.

Python jest językiem, który posiada w sobie wiele powszechnie rozwijanych przez społeczność bibliotek. Z tego względu mogą wyniknąć problemy podszywania się pod poszczególne paczki, które realizują te same zadania z dodatkowym exploitem (https://www.zdnet.com/article/twelve-malicious-python-libraries-found-and-removed-from-pypi/). W linuxie środowisko graficzne jest odpalane na Pythonie i trzeba pamiętać, żeby nie instalować niesprawdzonych paczek pod scieżką global-site. Gdy to zrobimy, biblioteka przygotowana przez atakującą osobę może mieć dostęp do wrażliwych części systemu. Najlepiej nie ruszać bibliotek Pythonowych przychodzących na przykład wraz z dystrybucją Ubuntu. Warto instalować pip lokalnie przy użyciu skryptu:

get-pip.py –user.

Wtedy jakąkolwiek bibliotekę należy instalować przy użyciu:

pip install requests –user.

Dzięki temu ryzyko dostania się do wrażliwych zasobów jest mniejsze.

Istotne jest również podnoszenie wersji Pythona, ponieważ pomiędzy wersjami są łatane dziury bezpieczeństwa oraz wycieki pamięci. Oczywiście nie zawsze możemy podnieść wersję napisanego już projektu, ze względu na różne ograniczenia środowiska, np. biblioteki albo współprace z innym teamem developerów, ale…to nie może być wytłumaczenie. Zawsze prowadźcie projekt w taki sposób, byście mogli korzystać z najświeższej wersji Pythona.

Deserializacja danych jest bardzo podatnym miejscem do przeprowadzania ataku. Szczególną uwagę należy poświęcić pickle’om i plikom yaml. W pierwszym przypadku nigdy nie mamy gwarancji, że to co odpakowujemy jest tym, czym faktycznie być powinno. Można oczywiście próbować się zabezpieczać przy użyciu hash’y na przykład md5 na plikach pickle’owych, ale mimo wszystko nadal istnieje ryzyko, że atakujący wykorzysta podatność funkcji hash’ujacych na kolizje nazw. O tym, że nie był to jednak nasz pickle dowiemy sie przy ładowaniu go do pamięci. Niestety będzie już za późno i zostanie wykonany exploit. Z tego powodu nie warto ich stosować w ogóle i przejść na przykład na serializację do JSONa, który można zabezpieczyć na przykład tokenem.

W przypadku plików yaml’owych warto pamiętać o tym, że jest to język na tyle elastyczny, że potrafi wywoływać w swojej strukturze funkcje systemowe, w tym Pythonowe. Przy ładowaniu tego typu plików warto użyć metody safe_load, która zabezpieczy nas przed wykonaniem pliku yaml.

Dużym zaskoczeniem może być zachowanie assert statement. Niektórzy programiści mają nawyk bronienia wykonania części kodu właśnie przy użyciu assert statement. Z takiego podejścia mogą wyniknąć spore problemy. Ciekawe zachowanie pojawia się dopiero wtedy, gdy odpalimy interpreter:

python -O,

który odpala kod w wersji zoptymalizowanej. Efekt jest taki, że każdy napotkany assert jest omijany i kod wykona się w całości. Assert statement najlepiej jest stosować wyłącznie w przypadku testów a w kodzie produkcyjnym wykorzystywać instrukcje warunkowe.

Bardzo złym nawykiem jest ufanie temu co otrzymujemy od użytkowników. Takie podejście umożliwia wstrzyknięcie kodu do części naszej aplikacji. Może to być kod SQL, komenda bash itd. Najlepszym nawykiem jest zawsze otaczanie w cudzysłów tego co przyjmujemy od użytkownika. Można też stosować filtry i white listy, żeby wykluczyć wkradnięcie się niechcianego kodu. Warto pamiętać, że część bibliotek ma swoje mechanizmy, żeby bronić się przed tego typu atakiem. Na przykład:

tree.xpath(„/tag[@id=$tagid]”, tagid=name).

Nie warto odkrywać koła na nowo we frameworku Django, zwłaszcza w dziedzinie bezpieczeństwa. Django framework ma poniższe zabezpieczenia przed atakami typu:

  • XSS
  • CSRF
  • Injection
  • Clickjacking

Możliwe też jest szyfrowanie połączeń protokołem SSL/HTTPS i korzystanie z najnowszych algorytmów tworzenia hash’y takich jak bcrypt. Warto używać Django ORM. W przypadku jednoczesnej potrzeby tworzenia query ręcznie z przyjmowaniem danych od użytkownika wystarczy pamiętać o cudzysłowach wokół tego, co przyjmujemy z zewnątrz. W przypadku ataku XSS należy pamiętać, żeby stosować listę dowolnych znaków, które nasz formularz może przyjąć. Warto tutaj nadmienić, że lista niedozwolnych znaków byłaby cięższa do stworzenia ze względu na Unicode. Język JavaScript jest na tyle elastycznym językiem, że można stworzyć kod wykonywalny przy użyciu konkretnych znaków jsfuck.com.

Ostatnio mikroserwisy stają się co raz bardziej popularne z tego względu warto wspomnieć o kilku istotnych zaleceniach bezpieczeństwa z nimi związanych. Docker na serwerach produkcyjnych musi mieć dostęp do gniazd sieciowych, przez co znajduje się w grupie uprzywilejowanej. Ze względu na bezpieczeństwo kontener jest uruchamiany na zwykłym użytkowniku, z tym że sam Docker ma uprawnienia uprzywilejowane. Problem pojawia się jeżeli nasz obraz zostanie zbudowany w ten sposób, że aplikacja działa na uprzywilejowanym użytkowniku wewnątrz kontenera. Do działającego kontenera można podpiąć volume w dowolnym miejscu systemu i mieć dostęp do plików hosta. Dlatego każda aplikacja Dockera powinna działać na użytkowniku nieuprzywilejowywanym wewnątrz kontenera.

W artykule starałem się krótko omówić tematy poruszone podczas mojej prelekcji na spotkaniu Wroc.Py w styczniu 2019. Poniżej znajdziecie prezentację wraz z przykładami, które pokażą Wam, że wspomniane problemy są realne i nie należy ich bagatelizować. Zachęcam do samodzielnego zgłębiania tych tematów. Jest to bardzo ciekawe i pozwala wyrabiać dobre nawyki w programowaniu. Niech kod, który jest wytwarzany, będzie nie tylko wydajny i czytelny, ale też bezpieczny.

Pomimo początkowo relatywnie małej wiedzy na temat bezpieczeństwa w Python udało mi się uniknąć w dużej mierze wielu podatności na poszczególne ataki. Nauczyłem się postępować zawsze zgodnie z zaleceniami zamieszczonymi w dokumentacji do poszczególnych bibliotek. Zgłębienie tematu bezpieczeństwa utwierdziło mnie w przekonaniu, że zalecenia zamieszczone w dokumentacji są istotne i warto do nich sięgać oraz je utrzymywać. Dobrym źródłem informacji na temat aktaków i zabezbpieczeń jest lista OWASP TOP 10. Można na niej zauważyć że, większość problemów jest wciąż aktualna dla Pythona.

Prezentacja:

Najnowsze wpisy