Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

Mechanizm Service Workers – używanie i nadużywanie

09 maja 2016, 14:48 | Teksty | komentarzy 8
Mechanizm Service Workers został wprowadzony w najnowszych wersjach przeglądarek internetowych, by rozwiązać problem, z którym świat aplikacji webowych boryka się od dawna – mianowicie: jak aplikacja powinna się zachowywać w przypadku utraty połączenia z Internetem. Niniejszy artykuł ma na celu przybliżenie zasad działania Service Workers oraz wskazanie największych zagrożeń związanych z tym mechanizmem, którymi w szczególności powinni być zainteresowani twórcy stron internetowych.

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).

swrvice-workers_01

Rysunek 1. Odwołanie się do adresu test.worker.

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.

Rysunek 2. Lista Service Workerów.

Rysunek 2. Lista Service Workerów.

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ć.

Rysunek 3. Inna lista Service Workerów, z opcją „Unregister”.

Rysunek 3. Inna lista Service Workerów, z opcją „Unregister”.

 

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>.

Wskazówka: jeżeli używamy na swoich stronach JSONP, warto zadbać o ograniczenia do parametru callback, np. tylko znaki alfanumeryczne i kropka, maksymalna długość: 20 znaków.

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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. volf

    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.

    Odpowiedz
  2. kbkk

    zły odnośnik przy opisie rys 3.

    Odpowiedz
  3. Q

    HTTP/HTTPS/JS/CSS nie będzie bezpieczne do momentu gdy strony będą podpisywały wszystkie zasoby w celu weryfikacji ich integralności.

    Odpowiedz
  4. darek

    Dobry art. Deface strony + Service Workers może naprawdę namieszać. Trochę się namęczyłem, żeby wyrejestrować SW z FF. Dla potomnych: about:serviceworkers

    Odpowiedz
  5. Fajny art. Krótko i na temat. Dzięki

    Odpowiedz
  6. Adam

    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 ?

    Odpowiedz
    • Odpowiedz
    • Michał Bentkowski

      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”.

      Odpowiedz

Odpowiedz