Wróć do strony głównej
Angular

Angular & Electron cz. 1

Jak już wcześniej pokazywaliśmy na łamach naszego bloga, Angular niekoniecznie musi ograniczać się tylko do aplikacji webowych (PWA, Capacitor). Dzisiaj spróbujemy uruchomić naszą aplikację bezpośrednio na desktopie, a wykorzystamy do tego framework Electron.

W tym artykule przedstawimy Wam całościowy obraz tego typu aplikacji a w kolejnej częsci przeprowadzimy Was przez proces integracji full stackowej aplikacji i pokażemy w jaki sposób można wykorzystać możliwości NestJs w środowisku Electronowym. Omówimy również szeroko poruszane aspekty bezpieczeństwa tego typu aplikacji.

Zacznijmy od omówienia samego Electrona.

  1. Czym jest Electron?
  2. Architektura
    2.1. Remote
    2.2. IPC
    2.3. ngx-electron
    2.4. Preload
    2.5. PostMessage API
    2.6. Context Bridge
    2.7. Lokalny serwer
  3. Bezpieczeństwo
    3.1. Security checklist
    3.2. Electronegativity
    3.3. Electron hardener
  4. Przykłady niepoprawnej konfiguracji
    4.1. Node Integration
    4.2. Context Isolation
    4.3. Sandboxing
  5. Podsumowanie
  6. Przydatne linki

Czym jest Electron?

W kilku słowach jest to framework rozwijany przez programistów z GitHuba umożliwiający tworzenie natywnych aplikacji na Windowsa, Linuxa czy też macOSa przy użyciu technologii webowych. W praktyce jest to połączenie Chromium odpowiedzialnego za uruchomienie naszej aplikacji oraz NodeJs umożliwiającego wykonywanie natywnych operacji.

Przekształcenie istniejącej aplikacji w Electronową jest dziecinnie proste i sprowadza się do kilku kroków, o których szczegółowo powiemy w kolejnym artykule poświęconym integracji.

Ze względu na charakter frameworka, po raz kolejny mamy do czynienia z wieloplatformowym programowaniem z wykorzystaniem pojedynczego, bazowego kodu.

Architektura

Aplikacje Electronowe zbudowane są w oparciu o dwie fundamentalne koncepcje. Mowa o procesach renderer, odpowiadającym poszczególnym oknom aplikacji oraz o procesie main, operującym w środowisku NodeJs i mającym dostęp do natywnych funkcjonalności.

 

Schematyczne przedstawienie architektury aplikacji Electronowej.

Żeby jednak możliwe było wykorzystanie potencjału architektury naszej aplikacji, musimy w jakiś sposób zrealizować komunikację między tymi odrębnymi procesami. Istnieje kilka dostępnych podejść.

Remote

Na początku gdy Electron jeszcze raczkował, komunikacja między rendererem a mainem odbywała się za pomocą modułu remote pozwalającego m.in. na ładowaniu modułów NodeJs bezpośrednio z poziomu aplikacji.

Rozwiązanie to było kłopotliwe ze względów bezpieczeństwa oraz wydajnościowych i zostało koniec końców oznaczone jako deprecated. Więcej informacji dlaczego tak się dzieje możecie znaleźć w artykule Jeremy’ego Rose’a.

Dokumentacja dotycząca modułu remote została całkowicie usunięta, wcześniej jednak autorzy sugerowali skorzystanie z IPC. Jak zatem powinniśmy zrealizować naszą komunikację?

IPC

Inter Process Communication to mechanizm umożliwiający komunikację między różnymi procesami w obrębie jednego systemu operacyjnego. W przypadku Electrona, wykorzystywany jest do wysyłania synchronicznych bądź asynchronicznych wiadomości pomiędzy rendererem a mainem (również w ramach samej implementacji Electrona).

Electron udostępnia nam moduły ipcMain oraz ipcRenderer za pośrednictwem których wysyłamy oraz nasłuchujemy na wiadomości. Aby jednak możliwe było bezpośrednie skorzystanie z tych modułów, aplikacja musi mieć włączoną flagę nodeIntegration.

Niestety pociąga to za sobą dość daleko idące konsekwencje, w przypadku luki w aplikacji atak XSS może nawet przerodzić się w RCE. Kwestię bezpieczeństwa związaną z tą flagą omawiamy bardziej szczegółowo w rozdziale Bezpieczeństwo.

ngx-electron

W przypadku aplikacji Angularowych mamy do dyspozycji paczkę definiującą serwisy do realizacji wspomnianej komunikacji. Paczka ta wykorzystuje pod spodem moduły IPC stąd nasza aplikacja również musi mieć włączoną flagę nodeIntegration.

Korzystając z tej paczki mamy dostęp do serwisu, za pośrednictwem którego jesteśmy w stanie wysyłać wiadomości do maina. Ponadto mamy również do dyspozycji kilka różnych API udostępnianych przez Electrona. Opis wszystkich dostępnych opcji znajdziecie w dokumentacji.

Preload

Uruchamiając naszą aplikację i tworząc jej główne okno, mamy możliwość wywołania tzw. skryptu preload. Z jego poziomu mamy dostęp zarówno do obiektu window jak i do środowiska NodeJs.

Popularną techniką było rozszerzanie obiektu window w ramach wspomnianego skryptu o dodatkowe metody umożliwiające wykonywanie operacji w środowisku NodeJs. Jest to możliwe, gdyż renderer oraz main posiadają współdzielony kontekst.

Z tego rozwiązania możemy skorzystać mając wyłączoną flagę nodeIntegration, dbając o bezpieczeństwo naszej aplikacji już na samym starcie.

W ramach konfiguracji naszego okna mamy również możliwość ustawienia flagi contextIsolation (o której będziemy mówić w dziale Bezpieczeństwo). Aby móc skorzystać z tej techniki, flaga ta musi być wyłączona.

Jak się pewnie domyślacie, tego typu rozwiązanie również jest wątpliwe pod względem bezpieczeństwa. Podobnie jak w przypadku IPC, ataki typu XSS mogą się przeobrazić w RCE, o czym ponownie więcej mówimy w dziale Bezpieczeństwo.

PostMessage API

W celu realizacji komunikacji między mainem oraz rendererem możemy również wykorzystać PostMessage API.

Jak w przypadku preload, możemy również z tego skorzystać mając wyłączoną flagę nodeIntegration.

Implementacja sprowadza się do definicji metod nasłuchujących na wiadomości w ramach skryptu preload oraz w samej aplikacji.

Niestety według dokumentacji protokół ten nie wspiera źródeł typu “file://”, stąd podczas wysyłania wiadomości musimy przekazać wartość “*” (asterix) jako parametr dla targetOrigin, co samo w sobie może mieć konsekwencje w ramach ogólnego bezpieczeństwa.

Context Bridge

Każde z powyżej przedstawionych rozwiązań wymagało od nas pewnych kompromisów dotyczących bezpieczeństwa naszej aplikacji. Dotarliśmy w końcu do rozwiązania, które na chwilę obecną jest zalecane jeśli chodzi o komunikację procesów main oraz renderer.

Jak wcześniej wspomnieliśmy, włączenie flagi contextIsolation powoduje, że obiekty window w obu procesach są różne i rozszerzanie nie ma najmniejszego sensu.

Aby jednak dalej było to możliwe, Electron udostępnia tzw. Context Bridge, za pomocą którego w bezpieczny sposób możemy definiować swego rodzaju API dla naszej aplikacji, z którego z kolei możemy korzystać w rendererze.

Lokalny serwer

Jako że podczas uruchamiania naszej aplikacji operujemy w środowisku NodeJs, nic nie stoi na przeszkodzie aby w tym momencie uruchomić lokalnie serwer, z którym nasza aplikacja będzie się komunikować za pomocą wybranego protokołu.

To rozwiązanie sprawia, że możemy skorzystać z dowolnych dostępnych frameworków do implementacji naszego backendu.

W ramach naszego artykułu, jako że Angular jest naszym ulubieńcem, skorzystamy z jego serwerowego odpowiednika czyli NestJs.

W przypadku tego rozwiązania trzeba mieć na uwadze, że serwer musi posiadać dobrze skonfigurowane własne zabezpieczenia. Możemy zatem stwierdzić, że bezpieczeństwo naszej aplikacji zależy w dużej mierze od poziomu zabezpieczeń lokalnie uruchomionego serwera.

Wszystkie z wyżej wymienionych rozwiązań stosujemy w ramach integracji naszej przykładowej aplikacji z Electronem.

Zanim jednak zaczniemy, skupmy się jeszcze przez chwilę na bezpieczeństwie aplikacji Electronowych i dlaczego jest to takie istotne.

Bezpieczeństwo

Pomimo zalet jakie niesie ze sobą wykorzystanie Electrona, bezpieczeństwo tego typu aplikacji często jest przedmiotem krytyki. Najczęściej wynika to z nieprawidłowej konfiguracji, umożliwiającej ataki typu XSS (Cross-site scripting) czy nawet RCE (Remote Code Execution).

Często drugie wymienione są możliwe ze względu na charakter architektury aplikacji Electronowych. Jako że renderer ma dostęp do maina, z jego poziomu możliwy jest również dostęp do systemu operacyjnego. Wówczas gdy atakujący w jakiś sposób uzyska możliwość wywołania złośliwego kodu po stronie renderera, może eskalować zasięg tego ataku na system ofiary.

Praktycznie każda z większych aplikacji Electronowych była przez pewien czas podatna na tego typu ataki, zaliczając do nich klienta Steam, WhatsApp, Discord, Slack, Teams czy nawet VSCode.

Świetnym źródłem wszelkich aspektów związanych z bezpieczeństwem oraz atakami jest repozytorium awesome-electronjs-hacking, zbierające w jednym miejscu różne wykłady, filmy oraz artykuły na ten temat.

 

Czy rozpoznajesz wszystkie przedstawione aplikacje Electronowe?

Kwestia zabezpieczeń jest ponadto istotna ze względu na stale rosnące liczbę aplikacji Electronowych, w chwili pisania tego artykułu dostępnych jest ich oficjalnie aż 950.

Jak zatem zaradzić wysokim wymaganiom dotyczącym standardów bezpieczeństwa?

Security checklist

Aby ograniczyć liczbę niepoprawnie skonfigurowanych aplikacji oraz podnieść ich bezpieczeństwo, programiści Electrona stworzyli listę zalecanych praktyk oraz ustawień.

Zagadnieniu bezpieczeństwa została poświęcona cała odrębna sekcja w dokumentacji, która dokładnie tłumaczy poszczególne zalecane ustawienia.

Electronegativity

W ramach zapobieganiu nieprawidłowych konfiguracji dostępne jest również narzędzie Electronegativity. Jest to paczka npm’owa, która weryfikuje naszą aplikację pod kątem zalecanych zabezpieczeń oraz udostępnia dokumentację omawiającą znalezione problemy.

Świetne narzędzie w celu przeprowadzenia weryfikacji zabezpieczeń naszej aplikacji.

Electron secure defaults

Programiści z 1Password podczas tworzenia swojej aplikacji stworzyli szablon dla bezpiecznych aplikacji Electronowych.

Jest to doskonałe miejsce aby zacząć przygodę z tworzeniem tego typu aplikacji, mając na uwadze jak najwyższy poziom zabezpieczeń.

Electron hardener

Kolejne narzędzie stworzone przez ludzi z 1Password, mające na celu wyłączenie wszelkich dodatkowych flag z jakimi może zostać uruchomiona aplikacja Electronowa, które mogłyby prowadzić do ewentualnych dziur.

Jeśli chcemy aby nasza aplikacja była postrzegana jako bezpieczna, powinniśmy zdecydowanie skorzystać z tej biblioteki.

Przykłady niepoprawnej konfiguracji

Node Integration

Jednym z głównych założeń Electrona było połączenie możliwości aplikacji webowych z NodeJs w ramach jednej aplikacji. W tym celu programiści otrzymali dostęp do środowiska natywnego z poziomu renderera.

Sam zamysł jest jak najbardziej słuszny, niestety to podejście sprawia, że jakiekolwiek luki w bezpieczeństwie mogą prowadzić do RCE. Pojawiła się zatem możliwość wyłączenia bezpośredniej komunikacji między rendererem a mainem.

Mowa o fladze nodeIntegration, którą możemy ustawić podczas tworzenia okna. Od wersji v5 jest ona domyślnie wyłączona dla nowo utworzonych widoków.

Pojawia się natomiast kwestia w jaki sposób Electron ma spełniać funkcję desktopowych aplikacji nie mając dostępu do środowiska NodeJs? Jak już wcześniej wspominaliśmy, od tego czasu pojawiło się wiele alternatywnych podejść, które umożliwiają komunikację między procesami w bezpieczny sposób, np. Context Bridge,

Przykładowe wykorzystanie luki związanej z tą flagą: aplikacja Notable.

Context Isolation

Skrypt preload o którym wcześniej mówiliśmy oraz proces renderer przez długi czas domyślnie współdzieliły kontekst, tzn. przykładowo zmiany w obiekcie window w obrębie jednego procesu były widoczne w drugim.

Electron udostępnia nam flagę contextIsolation, za pomocą której możemy zlikwidować wspomnianą funkcjonalność, a od wersji v12 jest ona domyślnie włączona. Gdy Context Isolation jest aktywne, każdy proces renderer oraz skrypt preload posiadają własne kopie obiektów window, document itp. na których operują.

Współdzielenie kontekstu pomiędzy wspomnianymi procesami mogło być wykorzystane do przeprowadzenia udanych ataków XSS poprzez np. nadpisywanie prototypów.

Przykład wykorzystania luki: aplikacja Discord lub aplikacja WireApp.

Sandboxing

Jako że Electron wykorzystuje Chromium do renderowania aplikacji webowej, istnieje możliwość skorzystania dodatkowych zabezpieczeń zawartych w samym Chromium.

Jednym z nich jest sandboxing, czyli ograniczenie uprawnień danego procesu tak, aby nawet w przypadku udanego ataku, atakujący nie był w stanie eskalować swoich uprawnień i wywołać szkód w systemie ofiary.

Od wersji v5 flaga sandbox jest domyślnie włączona.

Podsumowanie

Po raz kolejny przedstawiliśmy Wam technologię dzięki której możemy tworzyć aplikacje działające na desktopach, nawet bez dostępu do sieci, a wszystko to w naszym ulubionym Angularze.

Musimy być jednak świadomi że istnieją potencjalne luki w bezpieczeństwie tego typu aplikacji.

Jeśli zastosujemy jednak zalecane podejścia oraz wszelkie środki ostrożności, myślę że możemy śmiało stwierdzić, że taka aplikacja będzie bezpieczna.

Mamy zatem nadzieję, że po przeczytaniu tego artykułu przetestujecie swoją nową wiedzę i stworzycie swoje własne, bezpieczne aplikacje desktopowe przy użyciu Angulara.

Przydatne linki

  1. Electron remote module considered harmful, https://nornagon.medium.com/electrons-remote-module-considered-harmful-70d69500f31
  2. Awesome ElectronJs hacking repository, https://github.com/doyensec/awesome-electronjs-hacking
  3. Electronegativity, https://github.com/doyensec/electronegativity
  4. Electron secure defaults, https://github.com/1Password/electron-secure-defaults
  5. Security checklist, https://www.electronjs.org/docs/tutorial/security#checklist-security-recommendations
  6. Sandbox, https://www.electronjs.org/docs/tutorial/sandbox
  7. Session, https://www.electronjs.org/docs/api/session
  8. Electron IPC and NodeIntegration, https://stackoverflow.com/questions/52236641/electron-ipc-and-nodeintegration

O autorze

Marcin Leśniczek

Wiecznie głodny wiedzy z pasją dla aplikacji mobilnych oraz hybrydowych. Zawsze otwarty na nowe pomysły i technologie, szukający dziury w całym. Po godzinach entuzjasta nauki i astronomii.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *