XSS w JavaScripcie: co to jest i jak się przed nim bronić

Mateusz Bogolubow

21-10-2026

(aktualizacja: 21-10-2026)

Czym jest XSS, dlaczego innerHTML z danymi użytkownika jest groźne i jak się bronić: textContent, DOMPurify, walidacja linków i Content Security Policy. Z przykładami sprawdzonymi w przeglądarce.

XSS w JavaScripcie: co to jest i jak się przed nim bronić
XSS (Cross-Site Scripting) to atak, w którym cudzy kod JavaScript wykonuje się na Twojej stronie, bo aplikacja wstawiła dane użytkownika do HTML bez zabezpieczenia. Najprostsza obrona to wstawianie tekstu przez `textContent` zamiast `innerHTML`, a gdy musisz wstawić HTML, przepuszczasz go przez bibliotekę sanityzującą, taką jak DOMPurify. Dodatkową warstwą jest nagłówek Content Security Policy, który blokuje skrypty spoza dozwolonych źródeł.

Większość aplikacji w pewnym momencie wyświetla coś, co napisał ktoś inny: komentarz, imię, nazwę produktu, opis profilu. Jeśli wstawisz taki tekst do strony nieostrożnie, ten ktoś może wstawić tam kod, który wykona się w przeglądarce każdego odwiedzającego. To jest XSS. Poniżej sprawdzamy, jak to wygląda w praktyce, co działa, a co jest mitem, i jak się bronić. Wszystkie przykłady uruchomiłem w prawdziwej przeglądarce (Chromium), a zamiast szkodliwego kodu używam bezpiecznego znacznika window.__xss, który tylko pokazuje, że kod się wykonał.

Czym jest XSS?

Skrót oznacza Cross-Site Scripting: kod JavaScript z zewnątrz uruchamia się w kontekście Twojej strony. Przeglądarka nie odróżnia go od Twojego własnego skryptu, więc kod ma dostęp do tego, do czego ma dostęp Twoja strona: do zawartości DOM, do localStorage i do document.cookie, a także może wysyłać żądania w imieniu zalogowanego użytkownika.

RodzajSkąd bierze się złośliwy kodPrzykład
Stored (zapisany)trafia do bazy i jest pokazywany każdemukomentarz pod artykułem z kodem w treści
Reflected (odbity)wraca w odpowiedzi na żądanie, np. z adresu URLstrona wyszukiwania wypisująca frazę z ?q=
DOM-basedpowstaje wyłącznie w przeglądarce, serwer go nie widziskrypt wstawia location.hash przez innerHTML

Dla początkującego frontendowca najważniejszy jest ostatni wiersz i ogólna zasada: to, co przyszło z zewnątrz, jest tekstem, nie HTML-em, dopóki świadomie nie zdecydujesz inaczej.

Jak wygląda podatny kod?

Wyobraź sobie listę komentarzy:

const out = document.getElementById('out');

function renderComment(text) {
  out.innerHTML = text; // text pochodzi od użytkownika
}

renderComment('<img src=x onerror="window.__xss = 1">');

Obrazek src=x nie wczyta się, więc przeglądarka uruchomi kod z atrybutu onerror. W moim teście w Chromium licznik window.__xss zmienił się na 1, czyli cudzy kod się wykonał. W prawdziwym ataku w miejscu zmiennej __xss byłoby coś, co czyta dane lub wykonuje akcje na koncie użytkownika.

Czy skrypt wstawiony przez innerHTML się wykona?

Tu jest popularny mit. Gdy wstawisz przez innerHTML zwykły znacznik <script>, przeglądarka go nie uruchomi. W moim teście w DOM pojawił się <script>window.__xss=1</script>, ale licznik został równy 0. Nie oznacza to jednak bezpieczeństwa. Przykład powyżej pokazuje, że wystarczy inny znacznik z atrybutem zdarzenia (onerror, onload, onclick), żeby kod wystartował. Dlatego filtrowanie samego słowa <script> to nie zabezpieczenie.

Jak się bronić przed XSS?

Czy textContent rozwiązuje problem?

W większości przypadków tak. textContent wstawia wartość jako zwykły tekst. Ten sam kod z textContent zamiast innerHTML:

function renderComment(text) {
  out.textContent = text;
}

W teście ten sam payload wyświetlił się na stronie jako widoczny tekst (&lt;img ...&gt; w DOM) i nic się nie wykonało. Jeśli budujesz elementy listy, kart czy komunikatów, użyj document.createElement razem z textContent. Różnice między sposobami zmiany treści elementu opisuje artykuł o DOM w JavaScripcie.

Co zrobić, gdy muszę wstawić HTML?

Czasem treść naprawdę zawiera formatowanie (edytor tekstu, komentarze z pogrubieniem). Wtedy przepuszczasz ją przez bibliotekę, która usuwa niebezpieczne elementy i atrybuty, a zostawia bezpieczne. Najpopularniejsza to DOMPurify:

import DOMPurify from 'dompurify';

out.innerHTML = DOMPurify.sanitize(
  '<b>pogrubienie</b><img src=x onerror=alert(1)>'
);
// w DOM: <b>pogrubienie</b><img src="x">

W teście (DOMPurify 3.4.17) pogrubienie zostało, a atrybut onerror zniknął. Nie pisz własnej sanityzacji wyrażeniami regularnymi: ataków jest zbyt wiele, a gotowe biblioteki są stale poprawiane. Pamiętaj też o aktualizacji biblioteki.

Dlaczego linki od użytkownika też są ryzykowne?

Adres w href może zaczynać się od javascript:. Wtedy kliknięcie wykonuje kod:

const a = document.createElement('a');
a.href = 'javascript:window.__xss = 1';
a.textContent = 'moja strona';
out.append(a);
// po kliknięciu: window.__xss === 1

W teście po kliknięciu licznik wzrósł do 1. Jeśli pozwalasz użytkownikom podawać adresy (profil, link do strony), sprawdź protokół:

function isSafeUrl(value) {
  try {
    return ['http:', 'https:'].includes(new URL(value, location.href).protocol);
  } catch {
    return false;
  }
}

isSafeUrl('javascript:alert(1)'); // false
isSafeUrl('https://devmentor.pl'); // true

Czym jest Content Security Policy i czy wystarczy sama?

Content Security Policy (CSP) to nagłówek HTTP, którym serwer mówi przeglądarce, skąd wolno ładować i uruchamiać skrypty. Przykładowa polityka:

Content-Security-Policy: script-src 'self'

Oznacza: wykonuj tylko skrypty z własnej domeny, a skryptów wbudowanych w HTML (w tym atrybutów onerror) nie. Gdy zastosowałem ją przy tym samym podatnym kodzie, <img onerror> został wstawiony do DOM, ale się nie wykonał, a konsola pokazała jedno naruszenie CSP. To mocna druga linia obrony. Ma jednak cenę: jeśli Twoja strona sama używa skryptów wbudowanych, przestaną działać, dopóki nie dodasz do nich jednorazowego klucza (nonce) albo nie przeniesiesz ich do osobnych plików. Błędy typu „Refused to execute inline script” to właśnie skutek takiej polityki. CSP traktuj jako dodatek do textContent i sanityzacji, a nie jako ich zamiennik.

Co atakujący może zabrać po udanym XSS?

To dobre pytanie, bo od odpowiedzi zależy, gdzie trzymasz wrażliwe dane. W moim teście strona ustawiła dwa ciasteczka i wpis w localStorage:

MiejsceCzy skrypt na stronie to odczyta
localStorage (np. token jwt-abc)tak, localStorage.getItem('token') zwrócił wartość
Zwykłe ciasteczko (motyw=ciemny)tak, widoczne w document.cookie
Ciasteczko z flagą HttpOnly (sid)nie, document.cookie go nie pokazał

Dlatego token uwierzytelniający w localStorage jest wygodny, ale przy każdym błędzie XSS wystarczy jedna linijka, by go skopiować. Ciasteczko HttpOnly chroni przed odczytem, ale nie przed tym, by atakujący wykonał żądania w Twoim imieniu z poziomu strony, i otwiera inny problem: CSRF, czyli sfałszowane żądania wysyłane automatycznie z ciasteczkiem. Szerzej o tokenach piszę w artykule o uwierzytelnianiu w FastAPI i JWT, a o CSRF w osobnym artykule o CSRF i ochronie aplikacji.

A co z frameworkami?

Nowoczesne biblioteki domyślnie traktują wstawiane wartości jako tekst. React escapuje wyrażenia w JSX, a szablony w Pythonie (Jinja2 we Flasku) mają automatyczne escapowanie włączone dla szablonów HTML. Ochrona wyłącza się wtedy, gdy sam ją obejdziesz: w React służy do tego właściwość o znaczącej nazwie dangerouslySetInnerHTML, a w szablonach filtr |safe. Jeśli widzisz te konstrukcje w kodzie, potraktuj je jak czerwoną flagę i sprawdź, skąd pochodzi wstawiana treść. Tego zachowania nie testowałem tu osobno, opieram się na dokumentacji frameworków.

Jakie błędy mentor poprawia najczęściej?

Uwaga mentora: najczęstsze źródło XSS to nie sprytny atak, tylko wygoda. Ktoś pisze lista.innerHTML =

  • ${imie}
  • `i działa, bo w testach imię to „Anna”. Zamień to nacreateElementitextContent`. Sanityzacja jest dla treści, które naprawdę muszą zawierać HTML.

    Poniżej cała lista pułapek w jednym miejscu:

    PułapkaSkutekJak uniknąć
    innerHTML z danymi z formularza lub APIwykonanie cudzego kodutextContent lub createElement
    Filtrowanie słowa <script>atak przez <img onerror> i inne atrybutysanityzacja biblioteką, a nie własnym filtrem
    href z adresem od użytkownikakliknięcie wykonuje kod javascript:sprawdź protokół (http:, https:)
    Token w localStoragekradzież przy każdym błędzie XSSrozważ ciasteczko HttpOnly i zabezpieczenie przed CSRF
    CSP włączona i zapomniananie działają skrypty wbudowanenonce albo osobne pliki, a nie unsafe-inline
    Zaufanie do „własnego” APIdane też mogą zawierać cudzy kodtraktuj każde dane z zewnątrz jak tekst

    Wiele tutoriali, także nasz o JSON Server w JavaScripcie, używa innerHTML do wyświetlania danych z lokalnego serwera. W ćwiczeniu na własnych danych to nie szkodzi, ale nie przenoś tego wzorca do aplikacji, w której dane wpisują inni.

    Co dalej?

    Atak, w którym cudzy kod trafia do przeglądarki, to tylko jedna z kilku podobnych klas problemów. Podobna zasada „nie sklejaj danych użytkownika z kodem” dotyczy zapytań do bazy: zobacz SQL injection i zapytania parametryzowane. Tę samą zasadę stosuje się do poleceń systemowych i eval, co opisuje artykuł o command injection i eval. Jeśli budujesz formularz z walidacją po stronie przeglądarki, uzupełnieniem jest artykuł o walidacji formularza w JavaScripcie, a przy wgrywaniu plików przez użytkowników zajrzyj do tekstu o uploadzie plików po stronie frontendu. Jeśli chcesz nauczyć się budować bezpieczne interfejsy z mentorem, który ocenia Twój kod, sprawdź kurs JavaScript.

    Konsultacja i warsztaty devmentor.pl

    Masz pytania?