Wyświetlenie tekstu wpisanego przez użytkownika wydaje się niewinne. Ale jeśli wstawimy go na stronę jako HTML, każdy znacznik w nim zostanie zinterpretowany — także taki, który uruchamia kod. To klasyczny atak XSS. Różnica między textContent i innerHTML to różnica między „pokaż jako tekst” a „zinterpretuj jako kod strony”. Na lekcji uczniowie sprawdzają ją na nieszkodliwych przykładach i uczą się bezpiecznego wstawiania danych.
Scenariusz jest przeznaczony na 45 minut dla klasy znającej DOM i zdarzenia — na przykład po lekcjach o DOM w JavaScript i zdarzeniach. Podobny temat po stronie PHP omawia lekcja o htmlspecialchars. Ćwiczenia wykorzystują nieszkodliwy ładunek, który tylko ustawia zmienną.
Przygotowanie stanowisk
Potrzebna jest przeglądarka z konsolą (F12) i edytor tekstu. Rozpakuj pakiet z sekcji materiałów do osobnego folderu. HTML (index.html) jest gotowy i taki sam w obu folderach — pracujesz w start/app.js. Po zapisaniu app.js odśwież stronę. Błędy pokazuje konsola. Gotowe rozwiązanie jest w folderze rozwiazanie. Serwer i Node.js nie są wymagane.
Cele i plan lekcji
Po zajęciach uczeń potrafi:
- wstawić dane na stronę właściwością
textContenti wyjaśnić, że znaczniki są wtedy tylko tekstem; - wykazać, że
innerHTMLinterpretuje znaczniki i wykonuje atrybuty zdarzeń, np.onerror; - bezpiecznie dodać element do listy (
createElement,textContent,append); - zabezpieczyć tekst przez zamianę znaków specjalnych na encje HTML.
| Czas | Praca na lekcji |
|---|---|
| 0–5 min | Problem: wpis w formie <b>Ola</b> |
| 5–15 min | textContent kontra innerHTML |
| 15–22 min | XSS przez onerror — nieszkodliwy przykład |
| 22–37 min | Trzy zadania i pomiary |
| 37–42 min | Zasady bezpiecznego wstawiania |
| 42–45 min | Pytania kontrolne |
Dwie właściwości, dwa zachowania
element.textContent = tekstwstawiatekstdosłownie. Wpis<b>Ola</b>zostanie wyświetlony jako sześć znaków<,b,>itd. i nie utworzy żadnego elementu.element.innerHTML = teksttraktujetekstjako kod HTML. Wpis<b>Ola</b>utworzy elementbz tekstem „Ola” wewnątrz.
To użyteczne, gdy sami piszemy fragment strony, ale niebezpieczne dla danych pochodzących od użytkownika, z adresu strony lub z serwera.
Dlaczego to groźne
Wpis <img src=x onerror="...kod..."> wstawiony przez innerHTML utworzy obraz, którego adres jest błędny, więc przeglądarka wykona kod z atrybutu onerror. Atakujący mógłby w ten sposób odczytać dane strony lub podszyć się pod użytkownika. W lekcji ładunek tylko ustawia zmienną window.zdarzenie, a skutek sprawdzamy w konsoli: po innerHTML zmienna istnieje, po textContent — nie. Skrypty wstawione przez innerHTML w znaczniku <script> same się nie uruchamiają, ale atrybuty zdarzeń (onerror, onload, onclick) — tak.
Jak wstawiać bezpiecznie
- Domyślnie używaj
textContent. - Nowe elementy twórz przez
document.createElement, a ich tekst ustawiaj przeztextContent, po czym dodawaj jeappend. - Gdy musisz użyć
innerHTML, wstawiaj tylko własne, stałe fragmenty — a dane użytkownika wcześniej zamieniaj na encje (&→&,<→<,>→>,"→",'→'). - Po stronie serwera dane dodatkowo zabezpieczaj przy wypisywaniu (w PHP funkcją
htmlspecialchars).
Zadania
Zadanie 1. textContent. Napisz przezTextContent(tekst) i wstaw do #wynik wpis <b>Ola</b>. Sprawdź, czy powstał element b (wynik.children.length).
Zadanie 2. innerHTML. Napisz przezInnerHTML(tekst) i wstaw ten sam wpis. Porównaj liczbę elementów w #wynik. Następnie wstaw nieszkodliwy ładunek <img src=x onerror="window.zdarzenie = 1"> przez oba sposoby i sprawdź w konsoli wartość window.zdarzenie.
Zadanie 3. Bezpieczna lista i encje. Napisz dodajDoListy(tekst) (createElement, textContent, append) i dodaj ten ładunek do listy. Napisz zabezpiecz(tekst) i sprawdź wynik dla <b>"O'Reilly" & Co</b>.
Dla chętnych: sprawdź, co zrobi wstawienie <script>window.zdarzenie = 2</script> przez innerHTML i wyjaśnij różnicę wobec onerror.
Rozwiązanie i wyniki
const pole = document.querySelector("#pole");
const wynik = document.querySelector("#wynik");
const lista = document.querySelector("#lista");
// Zadanie 1: wstawianie tekstu
function przezTextContent(tekst) {
wynik.textContent = tekst;
}
// Zadanie 2: wstawianie jako HTML (niebezpieczne dla danych od uzytkownika)
function przezInnerHTML(tekst) {
wynik.innerHTML = tekst;
}
// Zadanie 3: bezpieczne dodawanie elementu listy i zamiana znakow specjalnych
function dodajDoListy(tekst) {
const li = document.createElement("li");
li.textContent = tekst;
lista.append(li);
}
function zabezpiecz(tekst) {
return tekst
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
document.querySelector("#a").addEventListener("click", () => przezTextContent(pole.value));
document.querySelector("#b").addEventListener("click", () => przezInnerHTML(pole.value));
document.querySelector("#c").addEventListener("click", () => dodajDoListy(pole.value));
Wyniki sprawdzone w przeglądarce: po przezTextContent("<b>Ola</b>") w #wynik jest tekst <b>Ola</b> i 0 elementów potomnych; po przezInnerHTML("<b>Ola</b>") powstaje 1 element b z tekstem „Ola”. Ładunek <img src=x onerror="window.zdarzenie = 1">: po textContent zmienna window.zdarzenie pozostaje niezdefiniowana, po innerHTML jest równa 1. Dodany do listy przez dodajDoListy ładunek jest pokazany jako tekst (element li bez elementu img w środku). Funkcja zabezpiecz zamienia <b>"O'Reilly" & Co</b> na <b>"O'Reilly" & Co</b>.
Typowe błędy
| Objaw | Co sprawdzić |
|---|---|
| Znaczniki widoczne na stronie jako tekst | Użyto textContent; jeśli chcesz interpretować HTML, to własny, stały fragment można wstawić innerHTML. |
| Tekst użytkownika psuje układ strony | Wstawiono go przez innerHTML bez zabezpieczenia. |
Skrypt w <script> wstawiony przez innerHTML się nie uruchomił | To zamierzone; atrybuty zdarzeń (onerror) jednak działają — nie polegaj na tym jako zabezpieczeniu. |
| Zabezpieczenie znaków nie działa | Zamianę & należy wykonać jako pierwszą; inaczej encje zostaną zamienione drugi raz. |
replaceAll nie jest dostępne | Starsza przeglądarka; użyj replace z wyrażeniem regularnym z flagą g. |
Wstawianie wielu elementów przez += na innerHTML jest wolne i niebezpieczne | Twórz elementy przez createElement i dodawaj append. |
Pytania kontrolne i punktacja
- Czym różni się
textContentodinnerHTML? - Dlaczego
<img src=x onerror="...">jest groźny po wstawieniu przezinnerHTML? - W jakiej kolejności zamieniamy znaki specjalne i dlaczego
&jako pierwszy?
Odpowiedzi: textContent wstawia tekst dosłownie, innerHTML interpretuje go jako kod HTML. Bo błędny adres obrazu wywołuje zdarzenie onerror i przeglądarka wykonuje kod z atrybutu. & zamieniamy pierwszy, bo inaczej zamieniłby się także znak & w już utworzonych encjach (< stałoby się &lt;).
Proponowana punktacja (razem 10 punktów): zadanie 1 — przezTextContent 1 pkt, pomiar liczby elementów 1 pkt; zadanie 2 — przezInnerHTML 1 pkt, test ładunku w obu przypadkach 2 pkt; zadanie 3 — dodajDoListy 2 pkt, zabezpiecz 2 pkt; wyjaśnienie XSS 1 pkt. Zadanie dla chętnych nie jest wymagane do pełnej oceny.
Materiały do pobrania
- Pakiet JavaScript: materiały do lekcji, rozwiązanie i karta pracy
- Karta pracy ucznia — otwórz i wydrukuj
Rozpakuj ZIP przed otwarciem plików. Materiały są dostępne bez logowania; publiczne rozwiązanie służy nauce i samodzielnemu sprawdzeniu, a nie poufnemu sprawdzianowi.
Co dalej?
Następny krok to zabezpieczenia po stronie serwera, nagłówki CSP i obsługa danych użytkownika w aplikacjach. Ten materiał rozwija dział o bezpieczeństwie w kursie JavaScript w TablicaPro, gdzie znajdziesz prezentacje, ćwiczenia i testy do prowadzenia lekcji.
Źródła techniczne: textContent w MDN oraz innerHTML i bezpieczeństwo. Zobacz też lekcję o htmlspecialchars.
Poprowadź kolejne lekcje JavaScript
Pełny kurs: prezentacje, ćwiczenia i testy do pracy z klasą.
Kup dostęp do kursu JavaScript →