Bezpieczeństwo zależności w aplikacjach
Dziś opowiem Ci o tym jak zadbać o bezpieczenstwo zależności.
Zdecydowana większość aplikacji, które znamy i tworzymy, dość intensywnie korzysta z różnego rodzaju zależności.
Zresztą to nic dziwnego. To nawet korzystne z punktu widzenia projektowego i architektonicznego. Zamiast pisać implementacje pewnej funkcji od zera, lepiej skorzystać z gotowego komponentu. Na przykład z biblioteki Open Source.
Tworząc implementację nawet najprostszej biblioteki (np. do wysyłania zapytań HTTP do API), musimy w pełni za nią odpowiadać. To oznacza, że musimy ją odpowiednio przetestować i zadbać o jej utrzymanie. Zamiast tego możemy skorzystać z gotowej biblioteki, którą przed nami przetestowały tysiące innych użytkowników. Dzięki temu będziemy w stanie osiągnąć swój cel o wiele szybciej. To bardzo mądre podejście.
Ilośc zależności w systemach
Jednak czy jesteśmy świadomi, jaka jest skala użycia zależności w naszej aplikacji?
Weźmy na przykład popularny program VSCode. Jest on napisany w Java Script i korzysta z różnych zależności.
Jak myślisz, ze dwadzieścia? Czterdzieści?
Otóż sam VSCode posiada:
- 149 zależności bezpośrednich — czyli wymienionych w pliku
package.json
- z tego powstaje około 1000 zależności pośrednich — czyli finalnie pobranych do katalogu
node_modules
przechowującego wszystkie zależności
Wygląda to na dużo. Ale jak ma się ilość linii kodu napisanego przez twórców VSCode do linii kodu w zależnościach?
Jak myślisz, czego jest więcej?
Zależności — ponad 4 mln linii kodu
Kod aplikacji – 1,7 mln linii kodu
Zatem widzimy, że nawet na poziomie rozmiarów kodu, zależności dość mocno przeważają w zbiorze całego kodu tego systemu.
Być może nie jesteśmy tego świadomi, że w naszych systemach często te proporcje wyglądają dość podobnie.
Duża ilośc zależności
Aplikacje składają się w dużym stopniu z zależności.
Czy to dobrze, czy źle?
Oczywiście, że dobrze. Te 4 miliony linii kodu to coś, czego deweloperzy VSCode nie musieli sami napisać i przetestować.
Gdyby nie korzystali z żadnych zależności to (bardzo mocno upraszczając sprawę) cały projekt musiałby być tworzony przez ponad trzy razy dłuższy czas. Tak więc to korzystanie bardzo nam oszczędza czas i koszty stworzenia całego systemu.
Bezpieczeństwo zależności
Jednak z drugiej strony w przypadku VSCode jest to 4 mln linii kodu, który jest uruchamiany w ramach naszej aplikacji, z takimi samymi uprawnieniami i dostępami, jak nasz własny kod. Dlatego można powiedzieć, że dodając bibliotekę do systemu, musimy jej trochę zaufać. Możemy też zamiast czystej ufności (żeby nie powiedzieć “zamiast kupowania kota w worku”), zweryfikować to, czy możemy bezpiecznie dodać ją do naszego systemu. I to jest zdecydowanie lepszy pomysł.
Każda zależność w systemie rodzi ryzyko. Naszym zadaniem jest mieć to ryzyko pod kontrolą i być świadomym, z jakich bibliotek korzystamy i czy to dobra decyzja.
Pewnie zadajesz sobie teraz pytanie – co więc powinniśmy weryfikować, gdy chcemy dodać nową bibliotekę do naszego systemu?
Szybkie pytanie – co wybierzesz z tych dwóch możliwości:
- popularną bibliotekę ze znanymi podatnościami,
- mało znaną bibliotekę bez żadnych podatności.
Taka decyzja nie zawsze jest oczywista.
Szczególnie w tak podchwytliwym przypadku jak tu podałem. Dalej omówię dokładniej, dlaczego to tak podchwytliwy przypadek.
Weryfikacja zależności
W ogólności powinniśmy weryfikować dwa aspekty:
- obecne bezpieczeństwo zaleźności,
- perspektywę na bezpieczeństwo w przyszłości.
To pozwoli nam upewnić się, czy możemy bezpiecznie korzystać z tej biblioteki. Wszystko jasne, ale jak to zrobić?
Znane podatności
Podstawowy element weryfikacji to sprawdzenie, czy biblioteka nie zawiera żadnych znanych podatności. Jasne jest, że jeśli biblioteka posiada zaszytego backdoora lub pozwala wykonać niedozwolone akcje, psujące nasz system, to oczywiście nie chcemy mieć jej w swoim systemie. Co najmniej do momentu wyeliminowania tego problemu.
Możemy to sprawdzić w popularnych bazach
- Snyk DB https://security.snyk.io/
- CVE Details – https://www.cvedetails.com/
Gdy przeglądamy tego typu rejestry dla naszych paczek, powinniśmy upewnić się, że nie zawierają one żadnych podatności na odpowiednim poziomie oceny ryzyka.
Dopiero gdy biblioteka nie ma podatności na z góry zdefiniowanym poziomie (czyli na przykład Critical lub High wg. CVSS), możemy z niej skorzystać w naszym systemie bez obawy o jego bezpieczeństwo.
Ograniczenie publicznych rejestrów
Jednak nie jest to takie proste. Publiczne rejestry podatności mają kilka istotnych ograniczeń:
- deweloperzy publikują tam podatności, ale tylko wtedy gdy sami je znaleźli w aplikacji
To oznacza, że gdy chcemy skorzystać z mało popularnej biblioteki i nie widzimy żadnego wpisu rejestrach podatności, to nie wystarcza. Jeżeli mało osób używa tej biblioteki, może się okazać, że biblioteka zawiera podatność, ale nikt (spośród jej wszystkich dziesięciu użytkowników) jej jak dotąd nie wykrył. Wtedy taka podatność nie znajdzie się w rejestrze. W związku z tym nie możemy ślepo wierzyć rejestrom podatności.
To bardzo podobna sytuacja do wybierania restauracji na podstawie opinii jej gości. Jeśli jest to miejsce popularne, jest też większe ryzyko, że komuś danie nie zasmakuje. Większe ryzyko, że negatywny komentarz się pojawi. Jeśli to mała klimatyczna knajpeczka, to w ogóle opinii o miejscu będzie tylko garstka, a tym samym i tych negatywnych będzie znacznie mniej, ale wcale nie będzie to znaczyło, że jedzenie i obsługa są bez zastrzeżeń.
Co możemy na to poradzić?
Zawsze zostaje nam możliwość audytowania kodu, aby rzeczywiście zweryfikować bezpieczeństwo danej biblioteki. Jednak w praktyce jest to dość karkołomne zadanie. Głównie dlatego, że rzetelne zweryfikowanie kodu aplikacji zajmuje bardzo wiele czasu. Jeżeli dodatkowo pomnożymy ten czas przez ilość zależności, dochodzimy do olbrzymiej ilości pracy, której raczej żadna firma nie będzie chciała poświęcić, aby zadbać o lepsze bezpieczeństwo zależności.
Dlatego też możemy skorzystać z pewnego rodzaju uproszczeń, weryfikując dojrzałość biblioteki.
Dojrzałość zależności
Dojrzałość biblioteki mówi nam o tym, jaka jest szansa na to, że w przyszłości wybór tej biblioteki okaże się dobrym pomysłem.
Wyobraźmy sobie sytuację, że pewna biblioteka jest kluczowym elementem naszego systemu. Używamy jej w wielu miejscach i wiele funkcji bazuje na jej elementach. Pewnego dnia okazuje się, że twórca biblioteki Open Source nie chce już dłużej jej utrzymywać. Kończy jej rozwój, a co gorsza okazuje się, że kasuje tę bibliotekę z publicznych rejestrów.
Jakie to niesie skutki dla naszych systemów? Nagle okazje się, że nic nam się nie buduje, nie jesteśmy w stanie pobrać aktualnej wersji tej zależności. Cała nasza praca jest zablokowana, co generuje nie tylko koszty ze względu na przestój, ale również ze względu na potrzebę naprawy tej sytuacji. A jak powiedzieliśmy, naprawa nie jest łatwa, bo bardzo mocno od tej biblioteki zależymy.
Wydaje Ci się, że to dość abstrakcyjny przypadek? Trochę tak, jednak historia pokazuje, że nawet najdziwniej brzmiące scenariusze potrafią się zrealizować. Tak było i w tym przypadku. Podobnie wyglądała sytuacja z biblioteką left-pad
z 2016 roku – szczegóły zdarzenia znajdziesz w Wikipedii (https://en.wikipedia.org/wiki/Npm_left-pad_incident). Spowodowało to wiele problemów.
Spoób weryfikacji dojrzałości
Co więc powinniśmy sprawdzać podczas takiej weryfikacji biblioteki?
- czy biblioteka jest utrzymywana — można to sprawdzić poprzez analizę listy ostatnich commitów i tego jak często są nowe releasy,
- czy biblioteka ma więcej niż jednego aktywnego twórcę — jeżeli biblioteka jest aktualnie rozwijana przez co najmniej dwie osoby, wtedy redukujemy szanse, że po zaprzestaniu pracy przez jednego autora, cała biblioteka przestanie się rozwijać;
- czy posiada swoją społeczność — czy ludzie angażują się w dyskusje dookoła problemów (Issues). To daje większe zaufanie, ze biblioteka działa poprawnie (bo jest przetestowana przez większa liczbę osób). Również zaangażowane osoby chętniej opublikują znaleziony problem bezpieczeństwa w publicznych rejestrach;
- czy biblioteka ma odpowiednią dla nas licencję — niektóre biblioteki zabraniają użycia ich w komercyjnych projektach lub wymuszają udostępnienie kodu źródłowego aplikacji;
- czy aktualizuje wszystkie swoje zależności — im świeższe zależności, tym szybciej twórca jest w stanie zaktualizować swoją zależność, gdy zostanie w niej wykryta podatność;
- czy projekt podlega automatycznemu skanowaniu bezpieczeństwa kodu — tak, żeby wykryć potencjalne problemy już w pipeline’ach tej biblioteki;
- czy repozytorium nie zawiera plików binarnych (wykonywalnych)? – każdy plik wykonywalny jest trudno przeskanować i nie jesteśmy w stanie łatwo przeprowadzić jego analizy bezpieczeństwa;
- czy tylko wybrane osoby mogą wgrywać kod do głównego brancha — tak, żeby ochronić się przed złośliwym kodem;
- czy każdy kod podlega Code Review — tak jak poprzednio, żeby chronić się przed złośliwym kodem.
Lista wygląda przerażająco. Sprawdzanie tego za każdym razem, gdy chcemy dodać nową bibliotekę, brzmi jak ogromne zadanie. Na szczęście ktoś już o tym pomyślał. Istnieją narzędzia, które przeprowadzą tę weryfikację za nas i zwrócą nam tylko jednostkowe wyniki.
Narzędzie do weryfikacji dojrzałości
Takim przykładem jest OpenSSF Scorecard https://securityscorecards.dev/viewer/?uri=github.com/ossf/scorecard Analizuje on repozytorium i przeprowadza automatyczną ocenę dla każdego z elementów oceny.
Kiedy powinniśmy weryfikować bezpieczeństwo naszych zależności?
Przede wszystkim jak tylko chcemy dodać nową bibliotekę do naszego systemu. To idealny moment, bo często wtedy analizujemy różne biblioteki, a element bezpieczeństwa i dojrzałości może być czynnikiem decydującym. To jest idealny moment również ze względu na to, że w ten sposób możemy nie dopuścić do naszego kodu jawnie niebezpiecznej biblioteki i w ten sposób unikniemy narażenia systemu na poważne luki.
Jednak poza tym każdy system ma wiele zastanych zależności. Co więcej, parametry oceny naszych zależności, które wcześniej wymieniliśmy, mogą się zmieniać w czasie. Na przykład jeszcze rok temu biblioteka mogła być intensywnie rozwijana, a teraz może nic się w niej nie dziać od kilku miesięcy. Dlatego też powinniśmy weryfikować dojrzałość i bezpieczeństwo zależności również na bieżąco dla wszystkich zależności.
Oczywiście na bieżąco niekoniecznie musi oznaczać sprawdzanie tego codziennie. Tym bardziej ręcznie. Na szczęście istnieją do tego dedykowane narzędzia, które możemy wykorzystać, aby regularnie badać bezpieczeństwo zależności. O tym jednak powiemy sobie w [???]