Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Mechanizm Service Workers – używanie i nadużywanie
Service Workers nie jest pierwszym mechanizmem próbującym rozwiązać problem z dostępem offline do aplikacji webowych. Kilka lat temu zdefiniowano mechanizm AppCache, który pozwala developerom zdefiniować listę plików podlegających cache’owaniu. Aby skorzystać z tego rozwiązania, należało w elemencie html dodać atrybut manifest wskazujący na plik manifestu.
Wyglądało to następująco:
<!doctype html> <html manifest="manifest.appcache"> .... </html>
Natomiast sam plik manifest.appcache mógł na przykład zawierać następującą treść:
CACHE MANIFEST /styl.css /main.js /obrazek.png
W tym przypadku, plik manifestu instruuje przeglądarkę, że cache’owaniu powinny podlegać trzy konkretne pliki.
Praktyka pokazała jednak, że rozwiązanie AppCache jest mało elastyczne; sposób definiowania pliku manifestu jest częściowo niezgodny z „duchem Web”, nie wspominając o niektórych dziwnych zachowaniach implementacji, które sprawiały, że AppCache nie spełniał dobrze swojej roli w aplikacjach, będących czymś więcej niż pojedynczą stroną korzystającą z szeregu skryptów JavaScript. Wszystkie te problemy świetnie opisuje w swoim artykule Jake Archibald.
Aby dać twórcom więcej swobody w implementacji cache’u w aplikacjach webowych, zdefiniowano mechanizm Service Workers. Dzięki niemu zyskali oni możliwość kontroli pobierania wszystkich zasobów z domeny, dla której są one zainstalowane z poziomu JavaScriptu.
Jak działają Service Workers
Aktualnie, Service Workers są domyślnie wspierane w Chromie, zaś w przypadku przeglądarki Firefox niezbędne jest włączenie flagi dom.serviceWorkers.enabled w about:config.
Jeśli będziemy chcieli używać Service Workers w swojej aplikacji, musi ona działać w protokole HTTPS. Ze względu na potężne możliwości, jakie daje ten mechanizm, udostępnianie go przez HTTP groziłoby ryzykiem ataku man-in-the-middle umożliwiającego przejęcie stałej kontroli nad domeną w obrębie przeglądarki używanej przez użytkownika w momencie ataku. Wyjątkiem jest localhost – w którym to obostrzenie nie obowiązuje, co pozwala na przeprowadzanie testów.
Skrypt Service Worker wykonuje się w JavaScript w kontekście niezależnym od głównej strony, co oznacza, że nie ma z niego dostępu do drzewa DOM. Aby zdefiniować w aplikacji mechanizm Service Worker, należy w skrypcie strony umieścić następujący kod:
Listing 1. Definicja podstawowego Service Workera.
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(r) { console.log('ServiceWorker zarejestrowany.') }).catch(function(e) { console.log('Ups! Błąd przy rejestracji ServiceWorkera! '+e) }); }
W opisanym wyżej kodzie rejestrujemy Service Worker, definiując jego ścieżkę na /sw.js. Wszystkie akcje Service Worker wykonywane są w ramach zdarzeń (eventów): install, activate, fetch. tzn. w pliku /sw.js).
Oto przykładowy kod:
Listing 2. Podstawowa definicja Service Workera.
self.addEventListener('install', function(ev) { // Zdarzenie wywoływane po zarejestrowaniu Service Workera }); self.addEventListener('activate', function(ev) { // Zdarzenie wywoływane po aktualizacji pliku Service Workera }); self.addEventListener('fetch', function(ev) { // Zdarzenie wywoływane podczas próby pobrania zasobu });
Typowo, w zdarzeniu install wypełniamy cache jakimiś początkowymi danymi, w zdarzeniu activate bierzemy pod uwagę możliwe zmiany w cache’u (czyli np. przestajemy cache’ować jeden z zasobów), zaś zdarzenie fetch jest wywoływane w przypadku pobrania jakichkolwiek zasobów. Co ważne – zdarzenie fetch jest wywoływane zawsze przy próbie pobrania zasobu – bez znaczenia, czy jesteśmy offline, czy online. Co więcej: jeżeli – na przykład – plik index.html zawiera odwołanie do pliku obrazka z innej domeny, to zapytanie też przechodzi przez Service Workera! Takie zachowanie powinno od razu zapalić lampkę ostrzegawczą w głowie każdego testera bezpieczeństwa i naturalnym wydaje się pytanie: „co się stanie, jeżeli złośliwy użytkownik będzie mógł zdefiniować własnego Service Workera?”. Odpowiedź poznamy poniżej.
Przykład
Aby zrozumieć lepiej działanie Service Workerów, posłużymy się prostym przykładem.
1. Utwórzmy dwa pliki: index.html i sw.js.
a) Zawartość index.html
<!doctype html> <html> <head> <script> navigator.serviceWorker.register('/sw.js').catch(e=>console.error('Ups!' + e)) </script> </head> <body> Tutaj nic nie ma. </body> </html>
b) Zawartość sw.js
self.addEventListener('fetch', function(ev) { if (ev.request.url.endsWith('.worker')) { ev.respondWith(new Response('<strong>Ten URL istnieje!</strong>', {headers: {"Content-type":"text/html"} })); } });
2. Następnie, musimy oba pliki uruchomić na serwerze – proponuję w tym celu używać Pythona i jego prosty serwer WWW, wywołując polecenie:
python -mSimpleHTTPServer
Wówczas na porcie 8000 zacznie nasłuchiwać serwer listujący pliki z obecnego katalogu.
3. Wróćmy jednak do plików, które zdefiniowaliśmy. Mamy index.html, którego jedyną rolą jest zarejestrowanie Service Workera spod adresu /sw.js – skrypt z Service Workerem musi zawsze znajdować się w tym samym originie, co skrypt go wywołujący. Sam Service Worker z kolei działa dość prosto. Sprawdza, czy adres URL żądania kończy rozszerzeniem „.worker” – a jeżeli tak jest – zwraca w odpowiedzi pogrubiony tekst: Ten URL istnieje!.
4. Gdy wejdziemy z poziomu przeglądarki najpierw pod http://localhost:8000, a następnie pod adres http://localhost:8000/cokolwiek.worker – zobaczymy, że rzeczywiście dostaniemy odpowiedź z Service Workera (rysunek 1).
Ten bardzo prosty przykład pokazuje dobitnie, że dzięki Service Workerowi kontrolujemy treść odpowiedzi HTTP wykonywanych do dowolnych zasobów w ramach jednej domeny, co umożliwia także definiowanie odpowiedzi do zasobów, które nie istnieją (np. plik test.worker nie istnieje w ogóle na serwerze).
W ramach eksperymentu możemy spróbować usunąć z serwera plik sw.js, zrestartować przeglądarkę i tym razem sprawdzić, co się stanie po odwołaniu do innego zasobu, np. http://localhost:8000/test2.worker. Okaże się, że zostanie zwrócona treść zdefiniowana w Service Workerze – mimo tego że sam Service Worker już nie istnieje.
Wniosek: gdy chcemy zrezygnować z używania Service Worker w swojej aplikacji webowej, musimy pamiętać o tym, by go wyrejestrować.
Jak zatem wyłączyć Service Workera?
W Chrome pod adresem chrome://inspect/#service-workers możemy zobaczyć listę działających Service Workerów dla wszystkich domen (rysunek 2). Intuicyjnie może wydawać się, że po kliknięciu na przycisk „Terminate”, Service Worker zostanie zatrzymany. Okazuje się, że nie; http://localhost:8000/test3.worker nadal zwraca wynik z Service Workera, bowiem zostaje on na nowo uruchomiony przy próbie dostępu do zasobu z naszej domeny.
Przeglądarka Chrome ma jeszcze jeden specjalny URL pokazujący listę Service Workerów, mianowicie: chrome://serviceworker-internals/ (rysunek 3). Na tym ekranie pojawia się przycisk „Unregister” i dopiero po jego kliknięciu, Service Worker zostanie wyrejestrowany, a zdefiniowane wcześniej URL-e przestaną działać.
Problemy bezpieczeństwa
Pokazaliśmy powyżej przykład Service Workera, który podstawiał własną odpowiedź do zasobów, których URL kończy się na „.worker”. Pokazaliśmy też, że z punktu widzenia użytkownika końcowego, raz zdefiniowany Service Worker może być trudny do usunięcia.
W świetle tych faktów możemy wyciągnąć dwa wnioski:
- jeżeli znajdziemy w danej domenie błąd XSS (Cross Site Scripting), który pozwoli na zdefiniowanie własnego Service Workera, będziemy mogli w tej domenie podmienić każdą podstronę; innymi słowy: możemy przeprowadzić prawdziwie permanentnego XSS-a na wszystkich podstronach;
- nawet jeśli atakowany użytkownik zorientuje się, że coś jest nie w porządku, i tak może mieć problem z usunięciem złośliwego Service Workera.
W większości przypadków wprowadzenie złośliwego Service Workera nie będzie proste. Wymagane jest bowiem, żeby skrypt Service Workera zwracał nagłówek Content-Type: application/javascript. W większości aplikacji webowych, nie mamy możliwości wstrzyknięcia w tego typu zasoby. Wyjątkiem jest sytuacja, w której aplikacja udostępnia interfejs JSONP. Wtedy zazwyczaj mamy kontrolę nad parametrem callback, w którym bardzo często możemy umieścić dowolny kod JavaScript.
Na przykład: https://example.com/jsonp?callback=<złośliwy_kod>.
Wydaje się, że przeglądarki internetowe mogłyby robić więcej, jeśli chodzi o ochronę użytkowników przed złośliwym użyciem Service Workerów. Dotychczas w nowowprowadzanych mechanizmach, jak np. CORS, stosowana była zasada opt-in, tzn. jawne określenie w nagłówku HTTP zgody na pobieranie zasobów z naszej domeny przez inne domeny.
W przypadku Service Workerów nie ma możliwości, aby w jawny sposób zadeklarować, że tylko jeden URL może być używany jako skrypt. Chrome wysyła w zapytaniu dodatkowy nagłówek Service-Worker: script, więc ewentualnym zabezpieczeniem po stronie serwera mogłoby być odrzucanie wszystkich żądań z tym nagłówkiem (jeśli nie używamy u siebie w ogóle Service Workerów).
Podsumowanie
Mechanizm Service Workers umożliwia programistom aplikacji webowych definiowanie sposobu pobierania zasobów (np. gdy aplikacja znajduje się w trybie offline). Ze względu na jego potężne możliwości, błąd typu XSS może zostać wykorzystany do permanentnej podmiany zawartości wszystkich podstron.
Aby zabezpieczyć się przed atakiem z wykorzystaniem mechanizmu Service Workers, należy zadbać o to, by użytkownik nie miał zbyt dużych możliwości umieszczania treści bezpośrednio w skryptach JavaScript. Dodatkowym środkiem bezpieczeństwa może być zmiana konfiguracji serwera polegająca na odrzucaniu żądań z nagłówkiem Service-Worker: script.
–Michał Bentkowski, pentester w Securitum
W opisie jest błąd:
„Przeglądarka Chrome ma jeszcze jeden specjalny URL pokazujący listę Service Workerów, mianowicie: chrome://inspect/#service-workers (rysunek 3).”
Na rysunku 3. w pasku adresu jest inny URL i to on wydaje się być prawidłowy.
zły odnośnik przy opisie rys 3.
HTTP/HTTPS/JS/CSS nie będzie bezpieczne do momentu gdy strony będą podpisywały wszystkie zasoby w celu weryfikacji ich integralności.
Dobry art. Deface strony + Service Workers może naprawdę namieszać. Trochę się namęczyłem, żeby wyrejestrować SW z FF. Dla potomnych: about:serviceworkers
Fajny art. Krótko i na temat. Dzięki
Cześć!
Świetny artykuł. Próbuję się wgryźć w to na czym polegają SW i szczerze mówiąc to najklarowniejsze wprowadzenie jakie znalazłem. Mam pytanie: czy jest jakiś followup do tego artykułu ? Minęły 4 lata i interesuje mnie czy wymienione tu podatności nadal istnieją? czy odkryto jakieś nowe ?
spoko, obadaj też naszą książkę: https://ksiazka.sekurak.pl/ -> sporo tam takich rozdziałów :)
Podatności nadal istnieją; w zasadzie w temacie możliwości rejestracji złośliwych service workerów nie zmieniło się nic przez kilka ostatnich lat.
Nadal najlepszym zabezpieczeniem przed rejestracją złośliwego service workera jest ubijanie zapytań z nagłówkiem „service-worker: script”.