
← Powrót do bloga
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.
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.
Wstawione do Twojej strony skrypty są wykonywane z tym samym źródłem co Twój pakiet. W praktyce oznacza to często:
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.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 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.
To są zwykle podejrzani w kodzie React/SPA:
| Punkt ujścia | Ryzyko |
|---|---|
element.innerHTML, insertAdjacentHTML | Parsuje HTML; każdy tag/zdarzenie, które zezwala, może wykonać skrypt. |
dangerouslySetInnerHTML | To samo co powyżej — React nie sanityzuje. |
document.write | To samo. |
eval, new Function, setTimeout(string) | Bezpośrednie wykonanie skryptu. |
Adresy URL javascript: w href / src | Nawigacja lub ładowanie zasobu, które wykonuje się jako adres URL skryptu. |
Obsługiwane postMessage, które eval lub ustawią HTML z event.data | XSS, 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.
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.
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.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.
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.
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ż:
HttpOnly, Secure, SameSite=Lax lub Strict tam, gdzie przepływy pozwalają — zmniejsza wyciek plików cookie między stronami a klasycznym CSRF.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.
apps/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).apps/web/app/api/chat/route.ts: JSON parse, pusty check, limit MAX_MESSAGE_LENGTH — kształtowanie nadużyć, a nie XSS samo w sobie.apps/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”.mainSklonuj frontend-xss-demo, uruchom npm install i npm run dev → Vite służy aplikacji na http://localhost:5173.
/insecure z ładunkami z dokumentów; ten sam przepływ na /secure pokazuje sanitizowane dane wyjściowe.docs/xss/: przegląd, trzy przejścia wpływu (działania bez interakcji użytkownika, wewnętrzny phishing, porwanie sesji / przechowywanie), plus niebezpieczne wzorce i bezpieczne wzorce dla do's i don'ts na poziomie kodu.localStorage (patrz 03-session-hijacking) tak, abyś mógł zobaczyć, co może odczytać skrypt na stronie; niebezpieczne wzorce sugeruje również localStorage.getItem('auth_token') w DevTools, aby sprawdzić.Porównaj /insecure a /secure w Elementach i Konsoli: te same komponenty, różne traktowanie ciągu przed dotarciem do DOM.
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.