(Nie)bezpieczeństwo w Serverless
Mówiąc o serverless, prędzej czy później musi paść pytanie - a czy to jest bezpieczne? Na tak postawione pytanie, klasyczna odpowiedź brzmi “to zależy” 😊
Model FaaS (Function as a Service) pozwala na budowę aplikacji oraz usług bez konieczności zarządzania fizycznymi bądź wirtualnymi serwerami. Dzięki temu, odpowiedzialność za bezpieczeństwo sieci, serwerów, systemów operacyjnych, ich konfiguracji oraz aktualizacji spoczywa na dostawcy. Z drugiej jednak strony, w gestii dewelopera pozostaje bezpieczeństwo kodu, logiki oraz konfiguracji aplikacji. Podział odpowiedzialności jest zdefiniowany tym modelem.
Czym jest Lambda?
Aby zrozumieć wektory ataków na aplikację zbudowaną w architekturze serverless, musimy sobie najpierw uświadomić, że Lambda to w rzeczywistości środowisko uruchomieniowe dla naszego kodu, działające w kontenerze na obrazie Amazon Linux. W teorii środowisko to jest uruchomione oddzielnie dla każdego wywołania, tylko na czas obsługi naszego kodu, a następnie usuwane. Właśnie, w teorii… uruchomienie nowej instancji Lambdy zajmuje stosunkowo dużo czasu (tzw. cold start). Aby przyspieszyć wydajność obsługi kolejnych wywołań Lambdy, “stare” instancje nie są od razu zamykane i są wykorzystywane do obsługi nadchodzących wywołań, podczas gdy nowe instancje są jeszcze uruchamiane. Biorąc pod uwagę, iż wywołania nie są całkowicie od siebie odseparowane, a system plików instancji Lambdy wcale nie jest całkowicie w trybie read-only
(folder /tmp
umożliwia zapis) to przy odpowiednich warunkach istnieje możliwość “przejęcia” przez atakującego instancji Lambdy i wykonania złośliwego kodu w kontekście nowego wywołania 😮 Więcej o tym możesz przeczytać tu.
Jeżeli chciałbyś zobaczyć jak wygląda Lambda “od środka” bez grzebania w kodzie to zachęcam do odwiedzenia projektu: lambdashell.com. Autor zachęca to dowolnego atakowania jego środowiska (i do tego płaci 1000$ za każdą istotną podatność), choć jak się już pewnie domyślasz wprowadził tam kilka ograniczeń.
OWASP Serverless Top 10
Błędy w kodzie zdarzają się zawsze, niezależnie czy piszesz aplikację webową czy serverless. 10 najczęściej powtarzanych rodzajów błędów zostało opublikowane w ramach OWASP Serverless Top 10. Najczęstszymi błędami jest brak odpowiedniej walidacji danych wejściowych. Oprócz dobrze znanych błędów typu cross-site scripting (XSS), XML External Entity (XXE) czy błędów deserializacji w serverless pojawia się jeszcze nowy wektor ataku, zwany: “Event Injection”.
AWS Lambda może obsługiwać zdarzenia (ang. event sources) pochodzące z innych usług AWS takich jak np. S3, DynamoDB, SNS itd. Przypuśćmy, że nasza testowa aplikacja umożliwia użytkownikom upload plików do bucketu S3. Upload nowego pliku wywołuje zaś Lambdę, która z kolei dodaje nową nazwę pliku do tabeli w MySQL.
W rzeczywistości wysłanie przez użytkownika pliku tworzy następujace zdarzenie:
1 | { |
Deweloper Lambdy potrzebuje wyciągnąć nazwę obiektu “Plik testowy” i umieścić ją w tabeli MySQL. Tworzy więc następujący kod:
1 | let filename = decodeURIComponent(s3.object.key.replace(/\+/g,'%20')); |
Wyobraźmy sobie, że jeden z użytkowników wyśle plik o nazwie: 1");(delete * from tabela_plikow
. W takim wypadku do bazy danych zostanie wysłane poniższe zapytanie, które oczywiście usunie zawartość wszystkich plików w tabeli:
1 | INSERT INTO uploads (`file`) VALUES ("1");(delete * from tabela_plikow) |
Zasada, aby nigdy nie ufać danym wejściowym (lub innymi słowy zawsze walidować dane wejściowe) jest jak najbardziej na miejscu nawet w przypadku obsługi zdarzeń.
Przeanalizujmy inny przypadek. Aplikacja https://www.serverless-hack.me/ przyjmuje jako parametr ścieżkę URL do dokumentu .doc
, który następnie odpowiednio konwertuje za pomocą Lambdy. Okazuje się jednak, że przekazując odpowiednio zmodyfikowane żądanie możemy wykonać zdalny kod w środowisku na którym uruchomiona jest Lambda (podatność typu Remote Code Execution). Co atakujący może w tej sytuacji zrobić? Przykładowo może przejąć rolę Lambdy, której uprawnienia są zdefiniowane w function policy. Lambda przechowuje klucze dostępowe oraz token sesyjny podpiętej roli w zmiennych środowiskowych. Jeśli więc atakujący może wykonać zdalny kod, to nic nie stoi na przeszkodzie aby się do nich dostać:
Naduzyżycia uprawnień
Zdobywając AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
oraz AWS_SESSION_TOKEN
atakujący może tak dużo na ile pozwala mu podpięta polityka. Praktyka pokazuje, że deweloperzy nadają zazwyczaj wyższe uprawnienia niż jest to wymagane do prawidłowego działania ich aplikacji. Przykładowo we wspomnianym już projekcie lambdashell.com początkowo nadane były uprawnienia do zapisu w usłudze S3, co zostało wykorzystane do przejęcia strony projektu. W niektórych przypadkach możliwe jest także podniesienie uprawnień skompromitowanej roli i dalsza eksploitacja środowiska AWS. Zainteresowanym tym tematem Czytelnikom, szczególnie polecam projekt CloudGoat.
Warto tu nadmienić, że nadmiarowe uprawnienia nie są tylko wynikiem “lenistwa” dewelopera (w końcu poliltyka z "Action": "*"
gwarantuje brak jakichkolwiek problemów z brakiem wymaganych uprawnień, prawda?). Przykładowo w framework’u Serverless domyślnie wykorzystywana jest jedna rola dla wszystkich funkcji 😮 W takim podejściu uprawnienia roli muszą być na tyle szerokie aby zaspokojały potrzeby wszystkich funkcji. Pamiętaj drogi Czytelniku aby zawsze stosować jedną politykę per funkcję!!! A jeśli korzystasz z framework’u Serverless może pomóc Ci w tym dedykowana wtyczka Serverless IAM Roles Per Function.
Oprócz zasady najniższych uprawnień, dobrą praktyką jest także separacja na poziomie kont, czyli stosowanie (minimum) oddzielnego konta do testów i oddzielnego konta produkcyjnego. Dodatkowo warto też skorzystać z usługi AWS Organizations oraz Service Control Policies do ograniczenia uprawnień na poziomie organizacyjnym. Więcej o najlepszych praktykach w kontekście bezpieczeństwa AWS możesz znaleźć w moim darmowym poradniku.
Błędy w zewnętrznych bibliotekach
Fakt, że Twój kod jest wolny od błędów wcale nie oznacza, że jesteś bezpieczny. Podatności bowiem mogą pojawić się także w wykorzystywanych zewnętrznych bibliotekach. Warto tu nadmienić pewne badanie, w którym to udało się przejąć kontrolę nad kodem 14% wszystkich paczek NPM 😮 Definiowanie wersji paczki w konwencji:
1 | "nazwa paczki NPM": "^1.0.0." |
oznacza stosowanie najnowszej kompatybilnej wersji, czyli np. 1.1.0. Wyobraź sobie teraz sytuację, gdy właściciel repozytorium zmodyfikuje kod NPM tak by ten wysłał na jego serwer przy każdym wywołaniu zmienne środowiskowe Lambdy, w której korzystasz z jego paczki… A żeby nie pozostać gołosłownym to pewien badacz zrealizował ten scenariusz - zwróć uwagę na liczbę pobrań pomimo dość wymownej nazwy paczki “do-not-download-this-package” 😂
Oczywiście w zewnętrznych bibliotekach można znaleźć całą gamę podatności. Wyobraźmy sobie, że nasza aplikacja serverless korzysta z paczki humanize-ms do przeliczania czasu na milisekundy. W starej wersji tej paczki można było znaleźć podatność typu ReDoS, czyli błędu w wyrażeniu regularnym. Podatność ta, polega na tym, że atakujący może wysłać taką wartość, której czas przetwarzania przekroczy dozwolony timeout Lambdy (maksymalnie może on wynosić 15 minut). Biorąc pod uwagę, że płacimy za czas wykonania Lambdy, to wysłanie tysięcy lub milionów takich żądań może znacząco podnieść koszty, powodując tym samym realne straty. Można więc powiedzieć, że znana podatność Denial of Service w serverless ewoluowała do Denial of Wallet 😉
Monitorowanie podatności w zewnętrznych bibliotekach jest zadaniem dość trudnym, szczególnie w dużych systemach. Warto jednak wspomnieć, że możemy sprawdzić używane paczki npm pod kątem znanych podatności używając narzędzia npm audit. Narzędzie to odpytuje o znane podatności za każdym razem kiedy uruchomimy npm install
(choć może też być użyte ręcznie do skanowania lokalnych paczek). Po wykryciu podatności, komendą npm audit fix
możemy zainstalować sugerowane aktualizacje.
Alternatywnie, jeden z dostawców, który zajmuje się monitorowaniem podatności w zewnętrznych bibliotekach, oferuje całkiem wygodną integrację z Lambdą.
Podsumowanie
Bez dwóch zdań serverless otwiera przed nami szereg korzyści, ale nie możemy zapomnieć o zagrożeniach. Nawet jeśli “wierzysz” w bezpieczeństwo swojego kodu to musisz pamiętać, że zewnętrzne biblioteki mogą niepostrzeżenie narazić na kompromitację Twojego środowiska AWS, włącznie ze wszystkimi zasobami w nim się znajdującymi. Stosowany w serverless model “pay as you go”, choć pełen niewątpliwych zalet, może okazać się gwoździem do trumny Twojego biznesu, jeśli tylko pozwolisz na zbyt dużo Twojemu użytkownikowi. Dlatego niezależnie czy korzystasz z klasycznej architektury, czy serverless pamiętaj o wielowarstwowym podejściu do bezpieczeństwa. Zachęcam do dyskusji i kontaktu. Znajdziesz mnie na Twitterze i LinkedIn.