Wróć do strony głównej
Angular

Czy możemy w pełni zaufać sanitizerom HTML i jak pracować bez nich?

Sanitizery to biblioteki odpowiedzialne za ochronę naszych aplikacji przed atakami Cross Site Scripting (XSS). Stosuje się je w sytuacji, gdy potrzebujemy wyrenderować kod HTML, który przechowujemy w zwykłym stringu.

Sanitizery otrzymują ciąg kodu HTML jako dane wejściowe i parsują go, pozbywając się niebezpiecznych wpisów, które pozwoliłyby atakującemu na wstrzyknięcie niebezpiecznego kodu JavaScript lub CSS. W teorii brzmi to skutecznie, ale parsowanie kodu HTML jest bardzo trudnym zagadnieniem. Dlaczego?

W teorii kod HTML jest prosty: istnieją zagnieżdżone znaczniki, a każdy z nich ma inne atrybuty. Wszystko co musimy zrobić to napisać wyrażenie regularne, które dzieli kod po znakach „<” i „>” i sprawdza wszystkie możliwe miejsca, gdzie można wstrzyknąć niebezpieczny kod. Jednak jest tu kilka luk:

  1. Kod nie musi być poprawny, może brakować np. znaczników zamykających lub atrybutów, mogą być dodane zbędne znaki itp:
  2. Część kodu może być napisana w nietypowej notacji UTF-8:
  3. Kod JavaScript może nie być prosty do oznaczenia jako niebezpieczny bez głębszej analizy:
  4. Niektóre atrybuty mogą być nieznane dla programistów, którzy implementują sanitizer:
  5. Albo kod może być po prostu zaskakujący:

    Po więcej ciekawych przykładów zapraszam tutaj: https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html.

Jak sobie z tym poradzić?

Jak widać, napisanie sanitizera to bardzo trudne zadanie. Szczególnie, że przeglądarki są ciągle rozwijane, dodawane są do nich nowe funkcjonalności, a wraz z nimi kolejne podatności. Nikt nie jest w stanie zagwarantować, że wynik działania sanitizera to w 100% bezpieczny kod.

Poza tym, jak każde oprogramowanie, sanitazery mogą zawierać błędy, a do tak kluczowego zadania, jakim jest ochrona przed wstrzyknięciem dowolnego kodu do naszej aplikacji, potrzebujemy rozwiązania gwarantującego pełne bezpieczeństwo.

Jak więc możemy sobie z tym poradzić? Co możemy zrobić, aby nasz kod gwarantował 100% bezpieczeństwa? Nie możemy korzystać z rozwiązań, które nie gwarantują 100% bezpieczeństwa 🙂 Potrzebne jest inne podejście.

Ponieważ problem parsowania jest bardzo trudny, porzućmy go całkowicie. Zamiast renderować HTML z łańcucha, renderujmy go ze struktury, którą możemy bezpiecznie przekonwertować na elementy drzewa DOM. Zbudujmy zagnieżdżoną strukturę reprezentującą HTML, który chcemy renderować.

A następnie użyjmy ng-template do rekurencyjnego renderowania elementów:

Rendered formatted content.

Pełny przykład: https://stackblitz.com/edit/angular-ivy-iquuzz?file=src/app/app.component.ts.

Real life

Powodem, dla którego chcemy używać sanityzatorów, jest to, że pozwalamy naszym użytkownikom używać HTML. Aby skorzystać z opisanego powyżej rozwiązania, musimy przekonwertować kod HTML, który stworzył użytkownik, na naszą strukturę. Jest to bardzo łatwe do wykonania:

Kod tworzy poprawną, bezpieczną strukturę i sprawdza, czy atrybut „href” jest poprawny. Jest otwarty na obsługę nowych znaczników.

Pełny przykład: https://stackblitz.com/edit/angular-ivy-qtcyzo?file=src/app/app.component.ts.

Popularne edytory tekstu zwracają podobne struktury:

  • https://editorjs.io/ – Przykład można znaleźć od razu na stronie głównej:
    An example from editorjs.io page.
  • https://quilljs.com zwraca struktury o nazwie Blots (https://github.com/quilljs/parchment#blots):

    Output with Blots.
  • W https://draftjs.org (edytor tekstu stworzony przez Facebooka) programiści mają dostęp do obiektu ContenetState (https://draftjs.org/docs/api-reference-content-state). Zawiera on strukturę całego dokumentu.

Oto przykład konwersji Blots’ów z quill.js na zwykły obiekt:

Output for the quill.js example.

Pełny przykład: https://stackblitz.com/edit/angular-bywfc1?file=src/main.ts.

Korzyści

Praca na takiej strukturze daje nam trzy ogromne korzyści.

Po pierwsze, mamy pełną kontrolę nad tym, jak będą renderowane elementy. Możemy łatwo zamienić warstwę widoku na inną. Zamiast używać standardowych elementów (np. linków), możemy użyć własnych komponentów, które dodają nową funkcjonalność (np. wyświetlają link z odpowiednią ikoną).

Po drugie, treści w takiej strukturze mogą być łatwo ponownie wykorzystane w innych aplikacjach, także tych, które nie wykorzystują HTML do renderowania treści, np. w aplikacjach mobilnych możemy wykorzystać natywne komponenty mobilne.

Po trzecie, możliwości ataku XSS na ten kod są znacznie bardziej ograniczone.

Jak widać stosując to rozwiązanie otwieramy się na zasadę open-close SOLID’u: nasz kod będzie otwarty na rozszerzenia i zamknięty na modyfikacje.

Na przykład nie mielibyśmy problemu z dodaniem komponentu, który nie istnieje natywnie w HTML lub zmianą wyświetlania wcześniej przechowywanej zawartości. W przypadku przechowywania czystego kodu HTML musielibyśmy dokonać kilku “tricky” modyfikacji, aby zapewnić, że stworzony przez użytkownika kod HTML będzie zawsze przetwarzany poprawnie. Przy zastosowaniu struktury opisanej w tym artykule jest to bardzo łatwe do osiągnięcia.

Podsumowanie

Używanie sanitizerów jest bardzo prostym i dość powszechnym rozwiązaniem chroniącym przed atakami XSS. W tym artykule przedstawiłem zagrożenia z tym związane. Być może warto zainwestować więcej czasu na początku projektu w obsługę struktury innej niż HTML, aby w późniejszym etapie czerpać opisane powyżej korzyści.

O autorze

Szymon Skrzyński

Uwielbia tworzyć oprogramowanie webowe w każdym jego aspekcie: od infrastruktury, przez bazy danych, cache, backend, workery, systemy kolejek po frontend, UI i UX. Fan bezpiecznego, czystego kodu i prostych rozwiązań.

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 *