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ć onload, onclick 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:
- Sanityzacja sprawdzoną biblioteką, po stronie serwera.
- Osobna domena albo subdomena na treści od użytkowników.
Content-Security-Policyinosniffw nagłówkach.- Wyświetlanie przez
<img>, nie inline. - Wgrywanie ograniczone do zaufanych ról.
- 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.
- W
<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.