Bezpieczeństwo frontendowe: XSS, granice zaufania i demonstracja, którą możesz uruchomić

← Powrót do bloga

20 kwietnia 20267 min read

Bezpieczeństwo frontendowe: XSS, granice zaufania i demonstracja, którą możesz uruchomić

Jak XSS dociera do DOM, które API przeglądarki są punktami ujścia oraz metody łagodzenia, które działają w produkcji — sanityzacja, CSP, pliki cookie i łączenie CSRF.

bezpieczeństwofrontendreact

Jeśli ciąg znaków jest interpretowany jako markup lub skrypt zamiast inert tekst, ta zawartość jest wykonywana jako kod w źródle Twojej strony — z tymi samymi uprawnieniami co Twój pakiet. To jest XSS: nie błąd stylowania, a zdezorientowany zastępca między danymi a instrukcjami. Ten post stanowi techniczny kręgosłup — mechaniki ataku, punkty ujścia, poprawki, które skalują się, plus działająca playground (Vite + React na main; na żywo: playgrounds.lucascoliveira.com) oraz wskazówki do tego repozytorium Next.js, gdzie nagłówki i MDX zmniejszają domyślne ryzyko.

To samo źródło: co właściwie zyskuje atakujący

Skrypty wstrzyknięte do Twojej strony są wykonywane z tym samym źródłem co Twój pakiet. W praktyce oznacza to często:

  • document.cookie widoczność dla plików cookie nieoznaczonych HttpOnly (kradzież sesji / łańcuchowanie naprawy).
  • fetch() / XMLHttpRequest do Twojego API z otwartymi poświadczeniami, jeśli pliki cookie są wysyłane, a CORS pozwala na to — lub eksfiltracja tokenów przechowywanych w localStorage / sessionStorage, jeśli Twoja aplikacja je tam umieściła.
  • Odczyty DOM nieujawnionych danych osobowych wyświetlanych na stronie oraz zmiany interfejsu (fałszywe formularze, nakładki) do phishingu wewnątrz interfejsu aplikacji.

Zatem łagodzenie nie polega na „użyciu Reacta” — chodzi o nigdy nieprzypisywanie bajtów kontrolowanych przez atakującego do punktu ujścia, który parsuje HTML lub wykonuje skrypt, chyba że przejrzana procedura zmniejszyła je do bezpiecznego typu.

Przechowywane, odbijane i oparte na DOM XSS (mechaniki)

Przechowywane XSS — N zaufaany HTML (lub ładunek, który staje się HTML po rozszerzeniu szablonu) jest przechowywany (baza danych, pamięć podręczna, indeks wyszukiwania). Każdy użytkownik, który ładuje ten rekord, trafia do punktu ujścia. Wpływ na frontend: pierwsze renderowanie, które wykonuje innerHTML = row.body lub równoważne bez sanityzacji, wykonuje ładunek.

Odbijane XSS — Ładunek nigdy nie jest przechowywany; odbija się od serwera lub warstwy routingu do odpowiedzi. Klasyczny: ?q=<script>...</script> odbity do HTML bez kodowania. Ekwivalent SPA: location.search lub hash parsed po stronie klienta i zapisany do DOM bez kodowania. Naprawa jest taka sama: traktuj ciągi pochodzące z adresu URL jako dane; jeśli musisz je odbić, koduj dla kontekstu (patrz poniżej).

DOM-based XSS — Odpowiedź serwera jest „czysta”, ale skrypt po stronie klienta odczytuje dane kontrolowane przez atakującego (location, referrer, postMessage, wiadomości WebSocket) i przekazuje je do punktu ujścia. Przykład: eval("handle" + location.hash.slice(1)) lub element.innerHTML = decodeURIComponent(...). Statyczna analiza szablonów nie wystarczy; musisz sprawdzić każdą ścieżkę od danych niezaufanych do punktu ujścia.

Punkty ujścia: API, które zmieniają ciągi w wykonanie lub HTML

To są zwykle podejrzani w kodzie React/SPA:

Punkt ujściaRyzyko
element.innerHTML, insertAdjacentHTMLParsuje HTML; każdy tag/zdarzenie, które zezwala, może wykonać skrypt.
dangerouslySetInnerHTMLTo samo co powyżej — React nie sanityzuje.
document.writeTo samo.
eval, new Function, setTimeout(string)Bezpośrednie wykonanie skryptu.
Adresy URL javascript: w href / srcNawigacja lub ładowanie zasobu, które wykonuje się jako adres URL skryptu.
Obsługiwane postMessage, które eval lub ustawią HTML z event.dataXSS, jeśli origin nie jest sprawdzany lub event.data dociera do punktu ujścia bez bezpiecznego kontraktu — nie tylko błędy „niewłaściwego okna”.

Nie punkt ujścia domyślnie: textContent, createTextNode, normalne dzieci tekstowe Reacta, atrybuty, które React traktuje jako ciągi, gdy nie omijasz jego kodowania. Potoki Markdown stają się punktami ujścia, gdy emitują surowe HTML i przypisujesz ten HTML do DOM bez sanityzacji.

Uwaga demonstracyjna (ważna): W HTML5 węzły <script> wstawione przez innerHTML / dangerouslySetInnerHTML nie są wykonywane — parser nie wykonuje ich w sposób, w jaki mógłby to zrobić klasyczny odbity XSS. Aby zobaczyć wykonanie, gdy HTML jest wstrzyknięty, ładunki zazwyczaj polegają na obsługach atrybutów (np. onerror na img) lub podobnych. Playground XSS utrzymuje ładunki w oparciu o ustawienia wstępne (patrz payloadPresets.ts), dzięki czemu możesz przetestować przypadki, które faktycznie uruchamiają się po wstawieniu.

Łagodzenie 1: kodowanie odpowiednie dla kontekstu a sanityzacja

  • Jeśli interfejs użytkownika potrzebuje tylko zwykłego tekstu — Przypisz tekst z textContent, dzieci tekstowe Reacta lub MDX, które kompilują się do komponentów bez potoku HTML. Nie jest wymagany sanitizer; nie grasz w HTML.

  • Jeśli potrzebujesz bogatego tekstu (odstępy, listy, łącza) — Potrzebujesz albo ograniczonego języka znaczników skompilowanego do bezpiecznych elementów albo sanityzacji HTML z listą dozwolonych (tagi + atrybuty). Kodowanie (np. kodowanie HTML) jest dla umieszczania danych w węzłach tekstowych HTML; sanityzacja jest dla gdy musisz pozwolić na podzbiór HTML. Nie myl tych dwóch.

  • Obrona w głębi dla bogatego tekstu w prawdziwych produktach — Sprawdź/ sanityzuj przy zapisie (API odrzuca nieznane tagi, ograniczenia długości) i sanityzuj lub renderuj przez bezpieczną ścieżkę przy odczycie (warstwa renderowania). Przechowywanie może zostać przywrócone, uszkodzone lub zapisane przez inną wersję usługi.

Łagodzenie 2: DOMPurify (i jak go poważnie wykorzystać)

DOMPurify to sanitizer przeglądarki z domyślnym profilem; nadal możesz go skonfigurować dla swojego produktu:

  • ALLOWED_TAGS / ALLOWED_ATTR — Zacznij od minimum (p, br, strong, em, a z href tylko jeśli potrzebujesz łączy). Każdy dodatkowy tag to powierzchnia ataku.
  • ADD_ATTR / FORBID_TAGS — Jawne podejście bije „zezwalaj prawie wszystko”.
  • RETURN_DOM / RETURN_TRUSTED_TYPE — Preferuj węzły DOM lub dane wyjściowe w stylu TrustedHTML, jeśli integrujesz z Trusted Types.
  • Zainstaluj hak afterSanitizeAttributes — Usuń wartości href, które zaczynają się od javascript: lub dziwne typy MIME data:, jeśli zezwalasz na łącza.

W playground na main, playground XSS kontrastuje niebezpieczną ścieżkę renderowania z sanitizowaną ścieżką (lista dozwolonych DOMPurify) — ten sam interfejs użytkownika, inna polityka zaufania.

Łagodzenie 3: Content-Security-Policy (ograniczenia, a nie zastąpienie)

CSP zmniejsza co może wykonać, gdy coś prześlizgnie się przez niego. W apps/web/next.config.ts ta strona ustawia CSP z default-src 'self', ciasne object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'none', plus script-src / style-src z 'unsafe-inline', ponieważ Next.js App Router + MUI sx obecnie wymagają skryptów wbudowanych w tym ustawieniu — udokumentowane w kodzie. Kodowanie lub mieszanie oparte na haśle script-src usunęłoby ogólne zezwolenie na skrypty wbudowane, ale wymagałoby pośrednictwa do wstrzyknięcia kodów nonce na żądanie — warte planowania, jeśli rzadko wysyłasz HTML użytkownika.

Sprawdzenie rzeczywistości: CSP nie zastępuje sanityzacji dla HTML użytkownika; zawęża promień wybuchu (np. może zablokować hosty skryptów, których nie zezwoliłeś). Obsługi zdarzeń wbudowane (onerror itp.) nie są automatycznie neutralizowane tylko dlatego, że ustawisz CSP — 'unsafe-inline' na script-src jest powszechne w prawdziwych aplikacjach (w tym konfiguracji Next/MUI na tej stronie), a blokowanie obsługi zdarzeń zwykle wymaga jawnego script-src / script-src-attr (lub kodów/nazw mieszających), w zależności od przeglądarki i poziomu CSP.

Łagodzenie 4: pliki cookie i CSRF (para z XSS)

XSS może ominąć tokeny CSRF, jeśli token jest czytelny z DOM lub jeśli skrypt atakującego wysyła żądania z poświadczeniami. Zatem: priorytet napraw XSS; również:

  • Pliki cookie sesji: HttpOnly, Secure, SameSite=Lax lub Strict tam, gdzie pozwalają na to przepływy — zmniejsza wyciek plików cookie między stronami a klasycznym CSRF.
  • Modyfikujące punkty końcowe: para z SameSite plikami cookie, tokenami przeciw-CSRF lub nagłówkami niestandardowymi + polityką CORS, aby przypadkowe strony nie mogły wysyłać żądań z poświadczeniami.

Zadanie frontendowe: nie umieszczaj tajemnic w pamięci JS-dostępnej, jeśli to możliwe; użyj fetch z jawną polityką credentials, która jest zgodna z Twoim projektem API.

Ten kod (konkretny)

  • Nagłówki / CSPapps/web/next.config.ts: nagłówki bezpieczeństwa na /(.*); ciąg CSP zbudowany w kodzie z env- specyficznym script-src (dev unsafe-eval dla stosów React tylko tam, gdzie to konieczne).
  • API czatuapps/web/app/api/chat/route.ts: JSON parse, pusty check, MAX_MESSAGE_LENGTH cap — kształtowanie nadużyć, a nie XSS samo w sobie.
  • Blogapps/web/lib/blog/mdx.tsx: MDX z ustalonym mapowaniem komponentów (next-mdx-remote/rsc), a nie surowe ciągi HTML z CMS. Inny model zagrożenia niż „treść wiadomości z HTML”.

Porównywalne uruchomienie: co daje main

Skopiuj playgrounds, uruchom npm install i npm run devVite służy aplikacji na http://localhost:5173.

  • Strona na żywo: playgrounds.lucascoliveira.com — ta sama demonstracja co w repozytorium; użyj tego, jeśli wolisz nie uruchamiać lokalnie.
  • Interfejs Todo (tylko w pamięci) — Dodaj elementy na /insecure z ładunkami z dokumentacji; ten sam przepływ na /secure pokazuje sanitizowane dane wyjściowe.
  • Dokumentacja / wnętrza — Zacznij od README w repozytorium, a następnie sprawdź payloadPresets.ts i simulateXssImpact.ts dla konkretnych przypadków i mapowania „wpływu”.
  • Fałszywy token — Aplikacja przechowuje demo token w localStorage (patrz **03-session-hijacking)), dzięki czemu możesz zobaczyć, co skrypt na stronie może odczytać; insecure-patterns sugeruje również localStorage.getItem('auth_token') w DevTools, aby sprawdzić go.

Porównaj /insecure i /secure w Elementach i Konsoli: te same komponenty, różne procedury obsługi ciągu przed trafieniem do DOM.

Lista kontrolna (poziom wdrożenia)

  1. Inwentaryzacja punktów ujściarg "dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|eval\\(|new Function" w Twojej aplikacji i zależnościach.
  2. Bogaty tekst — Sanitizer na podstawie listy dozwolonych na każdej ścieżce do HTML; testuj jednostkowo z ładunkami takimi jak <img src=x onerror=...>, adresy URL javascript: — i pamiętaj, że innerHTML nie wykonuje <script> w sposób, w jaki sugerują to wiele ściągawki. Dla demonstracji wstawiania w stylu innerHTML, preferuj onerror na img (lub podobne); <svg onload> jest często niewiarygodny, gdy wstawiany w ten sposób.
  3. Parametry URL → DOM — Nigdy nie przypisuj wyszukiwania/hasza do HTML; jeśli musisz wyświetlić, tekst lub koduj dla kontekstu.
  4. Markdown — Sanityzuj po pełnej konwersji MD→HTML; zabroń surowego HTML w MD, jeśli produkt na to pozwala.
  5. CSP — Zaostrzaj stopniowo; użyj Report-Only na etapie testowania, jeśli jest to konieczne.
  6. Pliki cookie / API — Dopasuj SameSite, poświadczenia i strategię CSRF z backendem; zakładaj, że XSS i CSRF są połączone.

Podsumowanie

XSS to kontrola przepływu: dane przekraczające interpretację. Ochrona polega na typowaniu na granicy: zwykły tekst, bezpieczne komponenty strukturalne lub sanityzowany HTML z minimalną listą dozwolonych — plus CSP i semantyka plików cookie, które ograniczają to, co może zrobić zbłąkany skrypt. Playground sprawia, że ta granica jest widoczna: niepewna ścieżka renderowania a sanitizowana, plus wstępnie ustawione ładunki i mapowane zachowanie „wpływu” w repozytorium.