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 pomiędzy danymi a instrukcjami. Ten post stanowi techniczny kręgosłup — mechaniki ataku, punkty ujścia, poprawki, które skalują, plus działająca frontend-xss-demo (Vite + React na main; na żywo: xss.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

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

  • Widoczność document.cookie dla plików cookie, które nie są oznaczone jako HttpOnly (kradzież sesji / łańcuchowanie naprawy).
  • fetch() / XMLHttpRequest do Twojego API z otwartymi poświadczeniami, jeśli pliki cookie są wysyłane, a CORS na to pozwala — lub eksfiltracja tokenów przechowywanych w localStorage / sessionStorage, jeśli Twoja aplikacja je tam umieszcza.
  • Odczyty DOM nie tajnych danych PII renderowanych na stronie oraz zmiany interfejsu (fałszywe formularze, nakładki) do phishingu wewnątrz interfejsu aplikacji.

Zatem łagodzenie nie polega na „użyciu React” — chodzi o nigdy nie przypisywanie 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, uderza w punkt 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 przykład: ?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 audytować każdą ścieżkę od niezaufanych danych 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 zwalidowany 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 demo (ważna): W HTML5 wstawione węzły <script> za pomocą innerHTML / dangerouslySetInnerHTML nie są wykonywane — parser nie wykonuje ich w sposób, w jaki mógłby to zrobić klasyczny odbijany XSS. Aby zobaczyć wykonanie, gdy HTML jest wstrzyknięty, ładunki zazwyczaj polegają na obsługach atrybutów (np. onerror na img) lub podobnych. Dokument insecure-patterns w repozytorium demo wyjaśnia to, abyś mógł przetestować aplikację z przykładami, 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, dziećmi tekstowymi 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) służy do umieszczania danych w węzłach tekstowych HTML; sanityzacja służy do gdy musisz pozwolić na podzbiór HTML. Nie myl tych dwóch.

  • Obrona w głębi dla bogatego tekstu w prawdziwych produktach — Waliduj/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 tylko z href, 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.
  • Zaimplementuj hak afterSanitizeAttributes — Usuń wartości href, które zaczynają się od javascript: lub dziwne typy MIME data:, jeśli zezwalasz na łącza.

W frontend-xss-demo na main, bezpieczna trasa (/secure) przeprowadza tekst todo przez DOMPurify przed dangerouslySetInnerHTML; niezabezpieczona trasa (/insecure) nie — ten sam interfejs użytkownika, inna polityka zaufania. Portugalski /seguro i /inseguro nadal istnieją jako stare aliasy i przekierowują do /secure i /insecure.

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

CSP zmniejsza co może wykonać, gdy coś prześlizgnie się. W apps/web/next.config.ts ta strona ustawia CSP z default-src 'self', ciasnym 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 hash-based 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.

Sprawdzian 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ługiwane zdarzenia (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 tej strony), a blokowanie obsługi zwykle wymaga jawnego script-src / script-src-attr (lub kodów/haszy), 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: priorytetyzuj poprawki XSS; również:

  • Pliki cookie sesji: HttpOnly, Secure, SameSite=Lax lub Strict tam, gdzie przepływy pozwalają — 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, jeśli to możliwe; użyj fetch z jawną polityką credentials, która jest zgodna z Twoim projektem API.

Ta baza kodu (konkretna)

  • 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 tylko dla stosów React, gdzie jest to potrzebne).
  • API czatuapps/web/app/api/chat/route.ts: JSON parse, pusty check, limit MAX_MESSAGE_LENGTH — 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

Sklonuj frontend-xss-demo, uruchom npm install i npm run devVite służy aplikacji na http://localhost:5173.

Porównaj /insecure a /secure w Elementach i Konsoli: te same komponenty, różne traktowanie ciągu przed dotarciem do DOM.

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. Demonstracja na main sprawia, że ta granica jest widoczna: /insecure a /secure, udokumentowane wpływy pod docs/xss/, oraz dyscyplina w każdym PR, który dotyka ciągi w pobliżu DOM.