Lekcja 19. Bezpieczeństwo

wrz 3, 2026 | Kurs SVG

Dlaczego SVG bywa traktowany jak plik wykonywalny — i słusznie

Przez osiemnaście lekcji powtarzało się jedno zdanie: SVG to tekst. Teraz zobaczymy jego drugą stronę.

Skoro to tekst, który przeglądarka interpretuje, to jest kod. A kod się wykonuje.

To nie jest teoretyczne rozważanie. To powód, dla którego WordPress domyślnie blokuje wgrywanie plików SVG, a większość serwisów przyjmujących awatary też ich nie przyjmuje.

Trzy powierzchnie ataku

Skrypty i obsługa zdarzeń. SVG może zawierać <script>, a każdy element może mieć onloadonclick czy onmouseover. To pełnoprawny JavaScript z dostępem do ciasteczek i sesji użytkownika.

Odwołania na zewnątrz. <image href="https://obcy.test/..."> pobierze zasób z cudzego serwera. Wystarczy do śledzenia, kto i kiedy otworzył plik, a bywa krokiem do poważniejszych rzeczy.

Treść skierowana nie do człowieka. Tekst w <desc><title> albo w niewidocznym elemencie jest częścią pliku, choć nikt go nie widzi. To ostatnio najciekawsza kategoria i wrócimy do niej w ostatniej lekcji.

Podgląd wygląda niewinnie — bo playground usuwa niebezpieczne elementy, zanim cokolwiek wyświetli. Po lewej widać, co plik naprawdę zawiera: skrypt wysyłający ciasteczka, trzy uchwyty zdarzeń, odnośnik javascript:, piksel śledzący i instrukcję w <desc> napisaną wyraźnie nie do Ciebie.

Ta sama grafika po sprzątaniu. Wizualnie nie do odróżnienia od poprzedniej. Właśnie dlatego ocena „na oko" nie działa, a plik z zewnątrz nigdy nie powinien trafiać na stronę bez przepuszczenia przez sanityzator.

Kiedy to naprawdę groźne

Nie każde użycie SVG jest ryzykowne. Ryzyko pojawia się w konkretnym momencie: gdy przyjmujesz plik od kogoś obcego i serwujesz go ze swojej domeny.

Awatar użytkownika. Logo klienta wgrane w panelu. Załącznik w zgłoszeniu. W każdym z tych przypadków ktoś z zewnątrz podaje Ci plik, który przeglądarka potraktuje jak dokument z Twojej domeny — z dostępem do sesji zalogowanych osób.

Sposób osadzenia zmienia bardzo dużo:

  • Przez <img> albo jako tło CSS — skrypty się nie wykonują. Przeglądarka renderuje grafikę w trybie ograniczonym, o czym była lekcja szesnasta. To domyślnie bezpieczniejsza droga.
  • Wklejony inline w HTML — to zwykły fragment dokumentu. Wszystko działa, łącznie ze skryptem.
  • Otwarty bezpośrednio pod swoim adresem — najgorszy przypadek. Plik staje się pełnoprawnym dokumentem w Twojej domenie, a wystarczy, że ktoś kliknie link.

To ostatnie bywa zaskoczeniem. Nawet jeśli u siebie zawsze wstawiasz grafiki przez <img>, plik i tak leży pod publicznym adresem — i ten adres można komuś podesłać.

Jak się zabezpieczyć

Sanityzacja to podstawa i nie da się jej niczym zastąpić. W PHP używa się biblioteki svg-sanitizer (na niej stoi wtyczka Safe SVG do WordPressa), w JavaScripcie DOMPurify z profilem SVG. Obie usuwają skrypty, uchwyty zdarzeń i niebezpieczne odwołania, zostawiając geometrię.

Nie pisz własnego sanityzatora do produkcji. Ten w playgroundzie ma kilkanaście linijek, usuwa skrypty i atrybuty on*, i to wystarcza do prezentowania kodu w kursie. Ale — jak widać w teście powyżej — przepuszcza zewnętrzny <image> i całą zawartość <desc>. Prawdziwa biblioteka pilnuje kilkudziesięciu takich przypadków, o których nikt nie pomyśli za pierwszym razem.

Serwuj cudze pliki z osobnej domeny. Grafika na pliki-uzytkownikow.example.com nie ma dostępu do ciasteczek z example.com, nawet gdy coś przecieknie przez sanityzację. To najskuteczniejsza pojedyncza bariera.

Nagłówki HTTP. Content-Security-Policy blokujący skrypty, X-Content-Type-Options: nosniff żeby przeglądarka nie zgadywała typu, a dla załączników Content-Disposition: attachment, które wymusza pobranie zamiast otwarcia.

Ograniczaj uprawnienia. Jeśli w WordPressie włączasz wgrywanie SVG, niech dotyczy administratorów, a nie autorów.

Bomba XML

Osobna, mniej znana kategoria: atak nie na wykonanie kodu, lecz na zasoby.

XML pozwala definiować encje, a encje mogą odwoływać się do siebie nawzajem. Kilkanaście zagnieżdżonych definicji rozwija się wykładniczo i plik ważący kilobajt potrafi zająć gigabajty pamięci przy parsowaniu. Klasyczna nazwa to „billion laughs".

Nowoczesne przeglądarki są na to odporne, ale biblioteki po stronie serwera już niekoniecznie — a to serwer parsuje plik, gdy generujesz miniaturę albo sprawdzasz wymiary. Dobre sanityzatory blokują deklarację DOCTYPE i encje, i to jest właściwe zachowanie.

Lista kontrolna

Przy przyjmowaniu plików z zewnątrz:

  1. Sanityzacja sprawdzoną biblioteką, po stronie serwera.
  2. Osobna domena albo subdomena na treści od użytkowników.
  3. Content-Security-Policy i nosniff w nagłówkach.
  4. Wyświetlanie przez <img>, nie inline.
  5. Wgrywanie ograniczone do zaufanych ról.
  6. Weryfikacja, że plik naprawdę jest SVG, a nie tylko ma takie rozszerzenie.

Przy plikach własnych, z programu graficznego, żaden z tych kroków nie jest potrzebny. Ryzyko wnosi pochodzenie pliku, nie format.

Jeszcze jedna pułapka, z tego kursu

Przy pisaniu presetów do tej lekcji natrafiłem na błąd, który wart jest wspomnienia, bo dotyczy dokładnie tej samej mechaniki.

Kod presetu zawierał literalny ciąg </script> wewnątrz stringu w JavaScripcie. Parser HTML nie wie nic o stringach — kończy blok <script> na pierwszym takim ciągu, gdziekolwiek go znajdzie. Cały skrypt został ucięty w połowie i widget przestał się budować.

Lekarstwo to zapis <\/script>, który w JavaScripcie znaczy to samo, a parsera HTML nie uruchamia. I ta sama zasada leży u podstaw połowy ataków typu XSS: treść wstawiona w kod może zmienić strukturę tego kodu, jeśli nikt jej nie przetworzy.

Podsumowanie

  • SVG to kod, więc może zawierać skrypty, uchwyty zdarzeń i odwołania zewnętrzne.
  • Ryzyko wnoszą pliki obcego pochodzenia serwowane z Twojej domeny, nie sam format.
  • <img> i w tle CSS skrypty się nie wykonują; inline i bezpośrednie otwarcie pliku są niebezpieczne.
  • Sanityzuj sprawdzoną biblioteką po stronie serwera; nie pisz własnej.
  • Osobna domena na pliki użytkowników to najskuteczniejsza pojedyncza bariera.
  • Bomba encji XML atakuje parser serwera, nie przeglądarkę.
  • Groźnego pliku nie odróżnisz wzrokiem od bezpiecznego.

Została ostatnia lekcja. Wrócimy w niej do zdania, które przewijało się przez cały kurs — i zobaczymy, co z niego wynika w świecie, w którym strony czytają nie tylko ludzie.


Do wypróbowania: w presecie plik z ładunkiem usuwaj po kolei elementy i patrz, w którym momencie podgląd się zmienia. Odpowiedź: nigdy, aż do prostokąta i koła. Wszystko pozostałe jest niewidoczne — i to jest cała lekcja.