
Błędy zarządzania zależnościami i jak ich unikać
Każdy obecnie tworzony system IT bardzo mocno polega na zależnościach. A my cały czas popełniamy błędy zarządzania zależnościami. Z jednaj strony jest to niezwykle wygodne. Zdejmuje to z naszej głowy myślenie o przetestowaniu danej funkcji (którą dostarcza ta zależność) oraz utrzymaniu jej. Nie oznacza to oczywiście, że już nie musimy testować całości systemu, jednak korzystanie z gotowych zależności pozwala nam na wzięcie gotowego elementu, który przetestował ktoś inny. To redukuje szansę na błędy po naszej stronie. Pamiętajmy – redukuje, ale nie eliminuje.
Z drugiej strony za każdym razem gdy dodajemy nową zależność do naszego systemu, pozbywamy się części kontroli nad naszym systemem. Czy to coś złego? Trochę tak. Nie wiemy, co kryje się w środku naszej zależności. Nie jesteśmy w stanie przewidzieć, czy jutro nie okaże się ona złośliwa.
Zależności a delegowanie pracy
Wyobraźmy sobie to na przykładzie delegowania komuś swojej pracy.
Gdy całość zadania wykonasz samodzielnie, to masz pełen wpływ na jego wynik i na jego jakość. Masz więc pewność, że wykonasz to zadanie zgodnie ze swoimi standardami.
Jeżeli jednak delegujesz część zadania do wykonania innej osobie, to już nie masz pewności jak dobrze to zadanie wykona ta inna osoba. Wobec tego będziesz musiał/musiała samodzielnie zweryfikować jego jakość.
Sprawa wygląda jeszcze trudniej gdy delegujesz różne kawałki danego zadania wielu różnym osobom. Wtedy musiał/musiałabyś zweryfikować pracę wszystkich tych osób. Można też im po prostu zaufać albo wybrać jakieś rozwiązanie pośrodku. Na przykład zweryfikować ich pracę tylko według określonego klucza.
Dokładnie tak wygląda sprawa z naszymi systemami. Każdy z nich deleguje część swojej pracy dziesiątkom różnych zależności. To trudna sytuacja. Dlatego trzeba być uważnym podczas korzystania z zależności w systemie. Zobaczmy jakich błędów w tym obszarze powinniśmy unikać. Niestety błędy zarządzania zależnościami zdarzają się stosunkowo często.
Mówimy tu zarówno o bibliotekach jak i zależnościach systemowych. Biblioteki to coś, co dodajemy do kodu aplikacji (np. Newtonsoft.JSON
lub React
), zaś zależności systemowe, to systemy z których funkcji korzystamy w naszym systemie (np. SendGrid jako serwis do wysyłki maili).
Tak więc jakich błędów powinniśmy unikać? Sama lista jest ułożona w kolejności, która ułatwia zrozumienie problemów. Nie jest to kolejność częstości występowania.
1 – Brak świadomości zależności
Pierwszy krok do zadbania o bezpieczeństwo jakiegokolwiek elementu systemu to jego świadomość. Nie możemy zabezpieczyć czegoś, czego w ogóle nie jesteśmy świadomi. Wydaje się, że to dość oczywiste stwierdzenie, ale niestety widzę, że w wielu projektach deweloperzy nadal nie są w pełni świadomi wszystkich zależności, które są przez ten system używane.
Niejednokrotnie zdarza się, że zależności systemowe są dobrze znane. Szczególnie te krytyczne, które są wymagane do poprawnego działania systemu. Jednak dopiero świadomość używanych bibliotek świadczy o tym, że zespół deweloperski najprawdopodobniej mocniej dba o ich bezpieczeństwo. W końcu jak już jesteśmy świadomi, że coś jest niebezpieczne, to prędzej czy później załatamy taką dziurę.
Brak świadomości jest bardzo wygodnym rozwiązaniem, bo może dawać złudne wrażenie, że nie mamy żadnych podatności w systemie. Jednak jest różnica pomiędzy brakiem podatności, bo ich nie szukaliśmy, a brakiem podatności, gdy zrobiliśmy dogłębną weryfikację systemu.
Pomocą dla nas może tu być generowanie plików SBOM (Software Bill of Material). Jest to plik, który dokumentuje zbiór zależności używanych przez nasz system. Jak dodatkowo zwizualizujemy go w odpowiednim narzędziu (jak na przykład Dependency Track), to będziemy mieć wygodną inwentaryzację wszystkiego, od czego zależymy w systemie.
2 – Brak regularnych aktualizacji zależności
W przypadku gdy korzystamy z zależności w postaci bibliotek mamy ten komfort, że dołączamy do naszego systemu ich konkretną wersję. To daje nam pewność tego, że jeżeli aplikacja działała podczas naszych testów (z konkretną wersją biblioteki), będzie ona również działała cały czas na Produkcji. Jednak pomimo, że to dość wygodne założenie, powinniśmy regularnie aktualizować nasze zależności.
Dlaczego? Gdy w bibliotece zostanie wykryta podatność bezpieczeństwa, to jak myślisz, która wersja tej biblioteki jako pierwsza będzie zawierała rozwiązanie tej podatności?
Najpewniej najnowsza.
W związku z tym warto tak projektować systemy, aby aktualizacja paczki była możliwie jak najprostsza. Będziemy sobie wdzięczni, gdyby w jakiejś bibliotece została wykryta krytyczna podatność. W końcu dużo łatwiej będzie odbić paczkę z wersji 4.5.6
na wersję 4.5.7
niż z 3.4.5
na 4.5.7
.
Inwestując regularnie czas w aktualizację paczek dbamy o to, że w sytuacji kryzysowej, gdy będziemy chcieli jak najszybciej załatać podatność, będziemy na to gotowi i ograniczymy sobie pracę do minimum. Dlatego właśnie nazwałem to inwestycją.
3 – Nie zamrożenie wersji zależności
Jak już mówimy o wersjonowaniu bibliotek, to koniecznie trzeba wspomnieć o niezwykle popularnym błędzie w niektórych ekosystemach paczek. Dla przykładu w npm, możliwe jest zdefiniowanie wersji zależności w dość elastycznym formacie.
3.4.5
– zawsze pobiera wersję3.4.5
^3.4.5
– odpowiednik3.4.*
– pozwala na automatyczne pobranie najnowszej wersji paczki, dla której zgadzają się dwa pierwsze komponenty wersji~3.4.5
– odpowiednik3..
– pozwala na automatyczne pobranie najnowszej wersji paczki, dla której zgadza się pierwszy komponent wersji.
Można powiedzieć, że to dobre zachowanie z perspektywy bezpieczeństwa. Zawsze podczas budowania pobierzemy najnowszą wersję paczki. A już mówiliśmy, najnowsza wersja ma zawsze większe szanse na poprawienie potencjalnych błędów bezpieczeństwa.
Jednak wyobraź sobie, co się stanie, gdy ktoś przejmie daną bibliotekę i wypuści kolejną wersję z zaszytą w kodzie l krytyczną podatnością, która może skompromitować nasz system. W sytuacji, gdy korzystalibyśmy z elastycznego wersjonowania w npm, przy kolejnym budowaniu, pobralibyśmy właśnie tę kolejną złośliwą wersję nawet o tym nie wiedząc.
Dodatkowo elastyczne wersjonowanie utrudnia inwentaryzację zależności, ponieważ każde budowanie systemu może pobrać różne wersje bibliotek.
4 – Osadzanie bibliotek w repozytorium
Jedną z metod na dodawanie bibliotek do systemu jest ich pobranie ze strony producenta i osadzenie jej w naszym repozytorium. To będzie działało, jednak może rodzić pewne problemy. Dlaczego? Ponieważ w ten sposób odcinamy się od całego ekosystemu zależności w danym języku programowania. A przez to dużo trudniej nam będzie zaktualizować daną bibliotekę.
Sprawa dodatkowo się komplikuje, gdy postanowimy taką bibliotekę zmodyfikować. To kuszące, w końcu pliki z kodem źródłowym znajdują się w naszym repozytorium. Jednak każda zmiana w bibliotece osadzonej w repozytorium sprawia, że nie będziemy w stanie w szybki sposób podbić wersji biblioteki. Szczególnie wtedy, kiedy najbardziej będzie nam zależeć na czasie – czyli podczas wykrycia podatności na Produkcji.
5 – Ślepe zaufanie zewnętrznym zależnościom
Każda nowa zależność w naszym systemie zabiera nam część kontroli nad tym, co tworzymy. Dokładnie tak samo jak w przykładzie z delegowaniem elementów zadania innym osobom. Każda taka sytuacja powinna wymusić na nas zweryfikowanie faktu, czy to dobrze, że korzystamy z danej zależności. Dotyczy to zarówno bibliotek jak i zależności systemowych. Każda taka zależność powinna mieć uzasadnienie, dlaczego dodajemy ją do naszego systemu. Powinniśmy również być w stanie zweryfikować bezpieczeństwo takiej zależności.
6 – Brak weryfikacji bezpieczeństwa
Jak to robić? Najlepiej zacząć od weryfikacji, czy dana biblioteka nie ma obecne znanych podatności. Najłatwiej sprawdzić to w publicznie dostępnych rejestrach takich jak Snyk DB (https://security.snyk.io/) lub CVE (https://cve.mitre.org/).

Obecność podatności o wysokiej istotności powinna zablokować użycie tej zależności w naszym systemie. Przynajmniej w określonej wersji. Powinniśmy wtedy wybrać wersję biblioteki bez podatności lub w ogóle z niej zrezygnować jeżeli to możliwe w rozsądnym okienku czasowym.
Warto tu jednak pamiętać, że rejestry znanych podatności ciągle żyją i ciągle dodawane są nowe podatności. To znaczy, że nawet jeżeli dziś dana biblioteka w określonej wersji nie ma podatności, to nie znaczy że nie ma ich w ogóle. Może się okazać, że jutro pojawią się w niej dwie nowe podatności.
7 – Brak weryfikacji dojrzałości biblioteki
Weryfikacja znanych podatności dotyczy obecnego stanu bezpieczeństwa danej paczki. Niezwykle istotne jest również zweryfikowanie jej perspektyw na bezpieczeństwo w przyszłości.
Co to oznacza?
Nawet gdy obecnie biblioteka nie ma podatności, to w każdej chwili mogą się one pojawić. Warto mieć poczucie, że w takiej sytuacji jej autorzy szybko dostarczą poprawkę bezpieczeństwa tego problemu. Bo co by było gdyby dzień po wykryciu krytycznej podatności w jednej z naszych krytycznych zależności, okazało się, że jej autor postanowił dłużej jej nie utrzymywać. W takiej sytuacji nie moglibyśmy liczyć na szybką poprawkę. Warto więc sprawdzić takie aspekty jak:
- szansa na dalsze utrzymanie,
- szczelność mechanizmów akceptowania zmian i publikowania,
- obecność trudnych do analizy elementów,
- obecność złośliwych workflowów przy budowaniu.
Pomocą tutaj może być serwis
https://securityscorecards.dev/viewer/?uri=github.com/imagemin/imagemin-pngquant
Pozwala on na szybką weryfikację dojrzałości danej biblioteki w podziale na określone, wymagane według standardu OpenSSF kategorie.

W ten sposób szybko i sprawnie możemy zweryfikować podstawowe aspekty bezpieczeństwa, bazując na analizie publicznego repozytorium. Narzędzie za pomocą zdefiniowanych zasad jest w stanie przeprowadzić analizę takich aspektów repozytorium jak:
- czy jest utrzymywana – tak, żeby wiedzieć, czy w razie problemów możemy liczyć na naprawę ich w rozsądnym czasie;
- czy zawiera niebezpieczne procesy budujące – żeby mieć poczucie, że żadne złośliwe zachowania nie zostaną wstrzyknięte podczas budowania biblioteki,
- czy kod podlega statycznej analizie – żeby wyłapać najprostsze zagrożenia na poziomie kodu,
- czy podlega Code Review – żeby zagwarantować, że każda zmiana wymaga akceptacji przez kogoś innego niż autor,
- czy merge do branchy zawsze wymagają zatwierdzenia – tak, żeby mechanizm Code Review był szczelny i dotyczył wszystkiego co jest w kodzie,
- czy w repozytorium nie ma plików wykonywalnych – są one trudne do analizy, więc mogą kryć w sobie niebezpieczne elementy.
W wyniku analizy otrzymujemy ocenę dojrzałości repozytorium w skali od 0 do 10. Jednak w praktyce to tylko element pomocniczy. Nie wszystkie elementy takiej weryfikacji są równie istotne. Na przykład dla bibliotek frontendowyh w npm, rzadkością jest stosowanie fuzzingu i nie jest to nic złego z punktu widzenia bezpieczeństwa.
8 – Brak monitorowania bezpieczeństwa i dojrzałości po fakcie
Gdy już zdecydujemy się na użycie danej zależności, to możemy poczuć się bezpieczni. Nic bardziej mylnego. Nasza praca z bezpieczeństwem zależności nie jest jeszcze skończona. Zarówno bezpieczeństwo zależności jak i jej dojrzałość może się zmieniać w czasie. Może się okazać, że jedyny twórca postanowi ją z dnia na dzień porzucić, albo ktoś wykryje krytyczną podatność w jej kodzie.
Tak samo w przypadku zależności systemowych. Nawet jeżeli w momencie decyzji o jej użyciu zewnętrzny system wyglądał porządnie, może się okazać, że w ciągu ostatniego roku zadziało się w nim wiele niepokojących rzeczy (wyciek danych czy też brak pomocy w zgłoszonych problemach).
Dlatego właśnie ważne jest regularne monitorowanie stanu zależności, z których korzystamy. Dzięki temu zadbamy o bezpieczeństwo systemu w każdym momencie jego życia.
Częstotliwość takiej weryfikacji zależy mocno od tego, co chcemy sprawdzić. Najczęściej powinniśmy sprawdzać rejestry podatności, to one najczęściej się zmieniają. Dojrzałość biblioteki czy też zewnętrznego systemu może być już weryfikowana dużo rzadziej.
9 – Nie przejmowanie się licencjami
Każda twórczość udostępniona publicznie podlega prawom autorskim. Bez wyraźnej zgody nie mamy prawa jej użyć w naszym systemie. Dlatego też powstały licencje Open Source. Pozwalają one na wykorzystywanie dzieł stworzonych przez inne osoby w granicach określonych przez te licencje.
Za każdym razem gdy dodajemy nową bibliotekę do systemu powinniśmy być świadomi ograniczeń lub wymagań, które musimy spełniać jako osoby korzystające z tej biblioteki.
10 – Poleganie na zależnościach do elementarnych działań
Korzystanie z zależności jest wygodne. Jednak niektórzy korzystają z nich nawet zbyt często. Warto pamiętać, że każda kolejna zależność w systemie pozbawia nas części kontroli nad nim. Dodatkowo dokłada nam pewną pracę na utrzymanie tej zależności – aktualizacje, czy też weryfikowanie bezpieczeństwa.
Dlatego też każda zależność powinna mieć jasne uzasadnienie, dlaczego jej potrzebujemy oraz dlaczego decydujemy się na skorzystanie do tego celu np. z biblioteki. W niektórych przypadkach nie ma potrzeby korzystania z biblioteki, jeśli możemy coś zrobić sami. Idealnym do tego przykładem jest biblioteka is-odd
w npm. Ma tylko jedną funkcję – sprawdzanie czy liczba jest nieparzysta. Można to załatwić jedną linijka kodu, jednak mimo to tygodniowo jest pobierana przez 290 tysięcy projektów.
github.com/jonschlinkert/is-odd
