PHP · bezpieczeństwo wyjścia

Bezpieczne wyświetlanie danych w PHP — htmlspecialchars

htmlspecialchars, encje HTML i atak XSS na cudzysłowach w atrybucie. Funkcja pomocnicza h(), wyniki i typowe błędy.

45 minutPoziom podstawowyPliki i rozwiązania

Materiał redakcyjny TablicaPro · liceum i technikum

<?php
$wpis = "<script>alert(1)</script>";
echo htmlspecialchars($wpis);
&lt;script&gt;alert(1)&lt;/script&gt;
Tekst, a nie kod.

Gdy strona wypisuje tekst wpisany przez użytkownika bez zabezpieczenia, atakujący może podstawić własny kod HTML lub JavaScript — to atak XSS. Skutecznym i prostym zabezpieczeniem jest zamiana znaków specjalnych na encje HTML w miejscu wypisywania. Na lekcji uczniowie widzą różnicę między wypisaniem „na surowo” a przez htmlspecialchars i uczą się pułapki z atrybutami.

Scenariusz jest przeznaczony na 45 minut dla klasy znającej zmienne, funkcje i formularze w PHP — na przykład po lekcji o formularzach PHP. Ćwiczenia pokazują atak w bezpiecznym środowisku, na tekście wypisywanym w terminalu.

Przygotowanie stanowisk

Potrzebny jest PHP 8.x uruchamiany w terminalu (na przykład z pakietu XAMPP; sprawdź polecenie php -v). Rozpakuj pakiet z sekcji materiałów do osobnego folderu i przejdź do niego w terminalu. Skrypty uruchamiasz poleceniem php start.php; serwer WWW nie jest potrzebny, a wynik pojawia się w terminalu. Jeśli pobierasz pojedyncze pliki .php.txt ze strony zamiast ZIP, usuń końcówkę .txt lokalnie.

Cele i plan lekcji

Po zajęciach uczeń potrafi:

  • wyjaśnić, na czym polega atak XSS i dlaczego dane od użytkownika są niezaufane;
  • zamienić <, >, &, " i ' na encje funkcją htmlspecialchars;
  • wskazać, kiedy cudzysłowy i apostrofy muszą być zamieniane (atrybuty HTML);
  • napisać funkcję pomocniczą h() ze stałymi opcjami.
CzasPraca na lekcji
0–5 minProblem: komentarz z kodem <script>
5–15 minEncje HTML i htmlspecialchars
15–22 minAtrybuty, flagi, funkcja h()
22–37 minTrzy zadania samodzielne
37–42 minKiedy zabezpieczenie jest potrzebne
42–45 minPytania kontrolne

Dlaczego trzeba zabezpieczać wyjście

Przeglądarka traktuje każdy fragment wyglądający jak znacznik HTML jako znacznik. Jeśli komentarz <script>alert(1)</script> trafi na stronę bez zmian, skrypt się wykona w przeglądarce każdego odwiedzającego. Funkcja htmlspecialchars zamienia znaki specjalne na encje: & na &amp;, < na &lt;, > na &gt;, " na &quot; i (z flagą ENT_QUOTES) ' na &#039;. Przeglądarka wyświetli je jako zwykłe znaki, ale nie potraktuje jako kodu.

Zabezpieczenie stosujemy w miejscu wypisywania, a nie przy zapisie danych — ten sam tekst może trafić do HTML, do pliku tekstowego albo do JSON-a, a każdy kontekst wymaga innego zabezpieczenia. Szczególnie ważne są cudzysłowy: w atrybucie value="..." wpisany cudzysłów zamyka atrybut i pozwala dopisać własny, np. onfocus=.... Dlatego zawsze podajemy flagi ENT_QUOTES | ENT_SUBSTITUTE i kodowanie "UTF-8", najlepiej w jednej funkcji pomocniczej h(). Zabezpieczenie htmlspecialchars jest odwracalne (htmlspecialchars_decode), więc nie służy do ukrywania danych.

Zadania

Zadanie 1. Surowo i bezpiecznie. Napisz h(string $tekst): string (z flagami ENT_QUOTES | ENT_SUBSTITUTE i "UTF-8"). Wypisz tekst <script>alert("XSS")</script> na surowo i przez h().

Zadanie 2. Cudzysłowy. Dla tekstu O'Reilly & "Synowie" wypisz wynik h() oraz htmlspecialchars($cytat, ENT_NOQUOTES). Porównaj.

Zadanie 3. Atrybut. Zbuduj <input value="..."> z wartości " onfocus="alert(1) bez zabezpieczenia i z h(). Porównaj długość tekstu z zadania 1 przed i po zabezpieczeniu i sprawdź, czy da się go odwrócić.

Dla chętnych: porównaj htmlspecialchars z strip_tags dla komentarza Jan <jan@example.com> & Ewa.

Rozwiązanie i wyniki

<?php
function h(string $tekst): string
{
    return htmlspecialchars($tekst, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}

// Zadanie 1
$wpis = '<script>alert("XSS")</script>';
echo $wpis, "\n";
echo h($wpis), "\n";

// Zadanie 2
$cytat = "O'Reilly & \"Synowie\"";
echo h($cytat), "\n";
echo htmlspecialchars($cytat, ENT_NOQUOTES), "\n";

// Zadanie 3
$imie = '" onfocus="alert(1)';
echo '<input value="' . $imie . '">', "\n";
echo '<input value="' . h($imie) . '">', "\n";
echo strlen($wpis), " ", strlen(h($wpis)), "\n";
echo htmlspecialchars_decode(h($wpis)) === $wpis ? "odwracalne" : "nieodwracalne", "\n";

Program wypisze:

<script>alert("XSS")</script>
&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
O&#039;Reilly &amp; &quot;Synowie&quot;
O'Reilly &amp; "Synowie"
<input value="" onfocus="alert(1)">
<input value="&quot; onfocus=&quot;alert(1)">
29 51
odwracalne

Zwróć uwagę na zadanie 3: bez zabezpieczenia wpisany tekst zamyka atrybut value i dopisuje nowy atrybut onfocus, a po zabezpieczeniu cały tekst zostaje wartością pola. Zadanie dla chętnych (rozszerzenie.php) wypisuje:

Jan &lt;jan@example.com&gt; &amp; Ewa
Jan  & Ewa

Typowe błędy

ObjawCo sprawdzić
Znaki < znikają ze stronyDane wypisano bez htmlspecialchars — przeglądarka uznała je za znacznik.
Apostrof nadal psuje atrybutBrakuje flagi ENT_QUOTES (w starszych wersjach PHP domyślnie nie obejmuje apostrofu).
Podwójne encje &amp;amp;Dane zabezpieczono dwa razy; zabezpieczaj raz, przy wypisywaniu.
Polskie znaki zamieniają się w � lub pusteBrak "UTF-8" albo plik w innym kodowaniu.
Zabezpieczenie dodano przy zapisie do bazyTo zła warstwa; zapisuj dane surowe i zabezpieczaj przy wypisywaniu (a bazę chroń zapytaniami przygotowanymi).
Dane w JavaScript lub URL nadal niebezpiecznehtmlspecialchars dotyczy kontekstu HTML; URL i JS wymagają innych funkcji.

Pytania kontrolne i punktacja

  1. Co zrobi htmlspecialchars("<b>")?
  2. Dlaczego zabezpieczenie stosujemy przy wypisywaniu, a nie przy zapisie?
  3. Po co flaga ENT_QUOTES?

Odpowiedzi: Zwróci tekst &lt;b&gt;, który przeglądarka wyświetli jako <b>. Bo ten sam tekst może trafić do różnych kontekstów (HTML, JSON, plik), z których każdy wymaga innego zabezpieczenia. Zamienia także cudzysłowy i apostrofy, co chroni wartości atrybutów.

Proponowana punktacja (razem 10 punktów): zadanie 1 — funkcja h() z flagami 2 pkt, dwa wypisania 1 pkt; zadanie 2 — dwa wyniki 1 pkt, porównanie 1 pkt; zadanie 3 — atrybut bez i z zabezpieczeniem 2 pkt, długość i odwracalność 1 pkt, wyjaśnienie ataku 1 pkt. Zadanie dla chętnych nie jest wymagane do pełnej oceny.

Materiały do pobrania

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 walidacja danych z formularza, zapytania przygotowane do bazy i nagłówki bezpieczeństwa. Ten materiał rozwija dział o bezpieczeństwie aplikacji w kursie PHP w TablicaPro, gdzie znajdziesz prezentacje, ćwiczenia i testy do prowadzenia lekcji.

Źródła techniczne: htmlspecialchars w podręczniku PHP oraz XSS w OWASP. Zobacz też lekcję o formularzach PHP.

Poprowadź kolejne lekcje PHP

Pełny kurs: prezentacje, ćwiczenia i testy do pracy z klasą.

Kup dostęp do kursu PHP →

Zobacz program kursu