XXE: bezpieczeństwo parserów XML w praktyce
Jak encje zewnętrzne zamieniają parser XML w czytnik plików i klienta HTTP. Gdzie XML wchodzi niezauważony, jak wyłączyć DTD, testować i wykrywać XXE.
- AUTOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLIKACJA
- 25 lipca 2026
- CZAS CZYTANIA
- 17 min czytania
- TEMAT
- Pentest i AppSec
XXE bywa uznawane za podatność historyczną, bo „przecież wszyscy używają już JSON”. To złudzenie wynika z tego, gdzie XML dziś siedzi: nie w widocznym API, tylko w formatach dokumentów, fakturach elektronicznych, kanałach wymiany danych, protokołach federacji tożsamości i konfiguracjach. Aplikacja, która nie ma ani jednego endpointu przyjmującego XML, potrafi parsować XML kilkanaście razy dziennie — przy okazji odczytu arkusza kalkulacyjnego albo weryfikacji tokenu logowania.
Istota problemu jest zawsze ta sama. Standard XML zawiera mechanizm definicji typu dokumentu, a w nim encje zewnętrzne — konstrukcję, która każe parserowi pobrać treść spod wskazanego adresu i wstawić ją do dokumentu. Domyślnie włączona w części parserów zamienia bibliotekę do czytania danych w czytnik plików lokalnych i klienta HTTP działającego z uprawnieniami serwera.
Co dokładnie robi parser
Dokument XML może deklarować definicję typu dokumentu, a w niej encje — nazwane skróty rozwijane podczas parsowania. Encja wewnętrzna podstawia tekst zdefiniowany w samym dokumencie. Encja zewnętrzna wskazuje zasób: plik w systemie plików albo adres sieciowy. Parser, który obsługuje ten mechanizm, wykona odwołanie zanim aplikacja zobaczy jakiekolwiek dane.
Istnieją też encje parametrowe, używane wewnątrz samej definicji typu dokumentu. Są istotne, ponieważ pozwalają zbudować konstrukcję, w której treść pobrana z zewnątrz wpływa na dalsze przetwarzanie — to jest podstawa wariantów działających bez odsyłania wyniku wprost do atakującego.
Kluczowa obserwacja projektowa: przetwarzanie definicji typu dokumentu nie jest potrzebne w 99% zastosowań biznesowych. Aplikacje wymieniają dane, a nie dokumenty wymagające walidacji przez DTD. Dlatego najlepszą naprawą nie jest filtrowanie, tylko wyłączenie całego mechanizmu.
Skutki, w kolejności od najczęstszych
Odczyt plików z serwera. Klasyczny skutek: zawartość pliku konfiguracyjnego, klucza prywatnego albo pliku z poświadczeniami trafia do odpowiedzi aplikacji lub do komunikatu błędu.
SSRF z wnętrza sieci. Parser wykonuje żądanie do adresu wskazanego w dokumencie. Z perspektywy sieci wewnętrznej jest to zaufany klient, więc widzi usługi niedostępne z zewnątrz. W chmurze celem jest zwykle usługa metadanych instancji, a łupem — tymczasowe poświadczenia roli.
Odmowa usługi. Zagnieżdżone encje wewnętrzne rozwijają się wykładniczo i potrafią wyczerpać pamięć procesu w ułamku sekundy. Ten wariant nie wymaga żadnego dostępu do sieci ani plików, więc działa nawet tam, gdzie encje zewnętrzne są wyłączone, ale limity rozwijania nie zostały ustawione.
Kanał poza pasmem. Gdy wynik parsowania nie wraca do nadawcy, treść odczytana lokalnie może zostać wyprowadzona przez adres w żądaniu sieciowym, a sama obecność zapytania DNS jest potwierdzeniem podatności.
Wykonanie kodu. Rzadkie i zależne od konkretnego stosu — możliwe tam, gdzie parser obsługuje schematy adresów pozwalające na uruchomienie procesu. Traktuje się to jako przypadek szczególny, nie regułę.
Gdzie XML wchodzi niezauważony
To jest najważniejsza część tego tematu, ponieważ inwentaryzacja decyduje o skuteczności naprawy.
Dokumenty biurowe. Formaty pakietów biurowych to archiwa ZIP zawierające pliki XML. Każda funkcja „wgraj arkusz” albo „zaimportuj dokument” jest parserem XML w przebraniu. To samo dotyczy generowania podglądów i wyciągania metadanych.
SVG. Grafika wektorowa jest dokumentem XML. Upload awatara w formacie SVG, konwersja do rastra, generowanie miniatury — wszystko to przechodzi przez parser.
Faktury i wymiana dokumentów. Ustrukturyzowane faktury elektroniczne, komunikaty bankowe, formaty wymiany w logistyce i ochronie zdrowia są oparte na XML. Te ścieżki bywają najlepiej chronione formalnie i najsłabiej technicznie, bo powstawały jako integracje „z zaufanym partnerem”.
Federacja tożsamości. Komunikaty SAML to XML przetwarzany przed uwierzytelnieniem. To czyni parser jednym z najbardziej wyeksponowanych komponentów w całej aplikacji. Dodatkowo pojawia się tam niuans kolejności: jeżeli dokument jest parsowany przed weryfikacją podpisu — a musi być, żeby podpis w ogóle znaleźć — to podatność parsera jest osiągalna bez żadnych poświadczeń.
Kanały i importy. Import kanałów RSS/Atom, pliki mapy witryny, konfiguracje eksportowane z innych systemów.
Negocjacja typu treści. Endpoint zaprojektowany dla JSON bywa zbudowany na szkielecie, który przy nagłówku wskazującym XML uprzejmie użyje parsera XML. To jest częsty i zaskakujący wektor: aplikacja „nie przyjmuje XML”, dopóki ktoś o to nie poprosi wprost.
Naprawa: wyłączyć definicję typu dokumentu
Właściwa naprawa jest konfiguracyjna i wygląda podobnie w każdym stosie: odrzucaj dokumenty zawierające deklarację DTD, a jeśli to niemożliwe, wyłącz rozwijanie encji zewnętrznych i ładowanie zewnętrznych definicji.
W ekosystemie Javy oznacza to włączenie bezpiecznego przetwarzania w fabryce parsera oraz jawne zabronienie deklaracji DTD i encji zewnętrznych. Ustawienia trzeba powtórzyć dla każdej używanej fabryki: parsera drzewa, parsera strumieniowego, transformatora i walidatora schematów, ponieważ konfiguracja jednej nie wpływa na pozostałe.
W platformie .NET kluczowe jest ustawienie trybu przetwarzania DTD na zabroniony w ustawieniach czytnika oraz nieużywanie przestarzałych klas z domyślnym rozwiązywaniem zasobów.
W Pythonie standardowe moduły nie są odporne we wszystkich trybach, dlatego przyjętym rozwiązaniem jest biblioteka defusedxml, która wprost blokuje niebezpieczne konstrukcje. W przypadku lxml należy jawnie ustawić parser tak, by nie ładował definicji zewnętrznych ani nie rozwijał encji.
W PHP nowoczesne wersje mają bezpieczniejsze domyślne ustawienia niż dawniej, ale nadal warto jawnie kontrolować opcje ładowania zasobów zewnętrznych zamiast polegać na wersji interpretera.
W ekosystemie Node.js wybór parsera przesądza sprawę: biblioteki, które w ogóle nie implementują DTD, są rekomendowanym wyborem dla danych z zewnątrz.
W Go standardowa biblioteka nie rozwija encji zewnętrznych, co czyni ten stos wygodnym punktem odniesienia — nadal jednak obowiązują limity rozmiaru i głębokości.
Niezależnie od języka obowiązuje zasada: konfiguracja parsera jest częścią kodu i podlega przeglądowi. Najczęstsza regresja polega na tym, że ktoś dodaje nową ścieżkę przetwarzania z domyślnie skonfigurowaną fabryką parsera.
Ograniczenia zasobów i izolacja
Wyłączenie encji zewnętrznych nie chroni przed wyczerpaniem pamięci przez encje wewnętrzne. Potrzebne są limity: maksymalny rozmiar dokumentu, maksymalna liczba i głębokość rozwinięć, limit czasu parsowania oraz limit pamięci procesu.
Warto też potraktować parsowanie plików od użytkowników jak uruchamianie obcego kodu. Parser dokumentów biurowych i grafik powinien działać w osobnym procesie, bez poświadczeń, bez dostępu do usługi metadanych chmury i bez ruchu wychodzącego. Wtedy nawet skuteczne wywołanie encji zewnętrznej nie ma dokąd pójść.
Blokada ruchu wychodzącego jest tutaj wyjątkowo skuteczna, bo odbiera atakującemu zarówno SSRF, jak i kanał poza pasmem — czyli dwa z trzech głównych skutków.
Metodyka testu
Test zaczyna się od inwentaryzacji: gdzie w systemie w ogóle pojawia się XML. Warto przejść listę z poprzedniej sekcji punkt po punkcie i sprawdzić, które funkcje aplikacji przyjmują pliki, importy, dokumenty i komunikaty federacyjne.
Drugi krok to sprawdzenie negocjacji typu treści na endpointach, które formalnie obsługują JSON. Jeżeli szkielet aplikacji akceptuje inny format, to powierzchnia ataku jest większa, niż wynika z dokumentacji.
Trzeci krok to weryfikacja zachowania parsera w środowisku testowym, na dokumentach kontrolnych, w zakresie objętym zgodą. Interesuje nas, czy deklaracja typu dokumentu jest w ogóle akceptowana. Jeżeli dokument z DTD zostaje odrzucony na wejściu, temat jest zamknięty niezależnie od wersji biblioteki.
Czwarty krok dotyczy wariantów ślepych. Gdy aplikacja nie zwraca treści dokumentu, potwierdzeniem jest kontrolowana obserwacja rozwiązywania nazw lub żądania sieciowego w kierunku infrastruktury testowej. To wymaga uzgodnienia zakresu, ponieważ ruch wychodzący z produkcji bywa monitorowany przez zespół obrony i wywoła incydent — co, nawiasem mówiąc, jest dobrą wiadomością o dojrzałości detekcji.
Piąty krok to sprawdzenie komunikatów błędów. Parser potrafi ujawniać ścieżki systemowe i fragmenty plików w treści wyjątku, nawet gdy sama aplikacja nie zwraca zawartości dokumentu.
Dowód w raporcie powinien być minimalny: wykazanie, że parser sięgnął po zasób zewnętrzny, wystarczy do klasyfikacji ryzyka. Zaciąganie plików z poświadczeniami z produkcji nie jest potrzebne i tworzy problem prawny po stronie klienta.
Detekcja
Najlepszym sygnałem jest ruch wychodzący z komponentu przetwarzającego dokumenty. Usługa konwertująca arkusze nie ma powodu rozwiązywać nazw domenowych; każde takie zdarzenie to alert o niskim poziomie szumu.
Drugim sygnałem są wyjątki parsera z informacją o niedozwolonej deklaracji DTD. Ich pojawienie się oznacza, że ochrona zadziałała — ale też, że ktoś próbuje.
Trzecim jest czas i pamięć parsowania. Gwałtowny wzrost zużycia przy jednym żądaniu wskazuje na rozwijanie encji.
Czwartym są anomalie w rozmiarze i strukturze przesyłanych dokumentów, w szczególności pliki znacząco mniejsze od typowych, ale generujące długie przetwarzanie.
Czy migracja na JSON rozwiązuje problem
Częściowo. JSON nie ma mechanizmu encji ani definicji typu dokumentu, więc cała rodzina XXE znika. Zostają jednak inne kwestie: głębokie zagnieżdżenie prowadzące do wyczerpania stosu, duplikaty kluczy interpretowane różnie przez różne biblioteki, bardzo duże liczby oraz problemy z niespójnym parsowaniem między komponentami. Limity rozmiaru i głębokości obowiązują niezależnie od formatu.
Tam, gdzie XML zostaje — a w fakturach, dokumentach biurowych i federacji tożsamości zostaje na długo — jedyną skuteczną drogą jest twarda konfiguracja parsera.
Walidacja schematem bez definicji typu dokumentu
Częstym argumentem przeciw wyłączeniu definicji typu dokumentu jest to, że „musimy walidować strukturę dokumentów”. To nieporozumienie wynikające z mieszania dwóch mechanizmów.
Walidacja przez DTD jest mechanizmem starym i to właśnie on niesie encje. Walidacja przez schemat XML jest osobnym mechanizmem, znacznie bogatszym w typy danych, i nie wymaga zezwolenia na deklarację DTD w dokumencie wejściowym. Poprawna konfiguracja wygląda tak: dokument nie może deklarować własnej definicji typu, a schemat jest ładowany przez aplikację z lokalizacji lokalnej, kontrolowanej przez zespół.
Ten szczegół ma znaczenie także dla samego schematu. Schemat pobierany z adresu podanego w dokumencie oznacza, że atakujący wpływa na reguły walidacji i na to, dokąd serwer wykona żądanie. Schematy powinny być artefaktem wdrażanym razem z aplikacją, z rozwiązywaniem odwołań do lokalnego katalogu, a nie do sieci.
Podobnie wygląda sprawa transformacji dokumentów. Silniki przekształceń mają własne mechanizmy pobierania dokumentów zewnętrznych i własne rozszerzenia funkcji. Jeżeli aplikacja przekształca dokumenty dostarczone przez użytkownika, konfiguracja procesora wymaga tej samej dyscypliny co konfiguracja parsera: zakaz odwołań zewnętrznych i wyłączone rozszerzenia.
Po potwierdzeniu podatności obowiązuje ta sama logika reakcji co przy innych błędach dających odczyt plików i żądania z serwera: należy założyć, że pliki konfiguracyjne w zasięgu procesu zostały odczytane, i rotować sekrety, a w środowisku chmurowym — unieważnić poświadczenia roli.
Checklista
Każdy parser XML w projekcie ma jawnie wyłączone przetwarzanie DTD i encji zewnętrznych. Konfiguracja jest powtórzona dla wszystkich fabryk: parsera, transformatora, walidatora schematu. Endpointy JSON nie akceptują innego formatu przez negocjację typu treści. Pliki biurowe i grafiki wektorowe są traktowane jak dokumenty XML. Parsowanie ma limity rozmiaru, głębokości, czasu i pamięci. Komponent przetwarzający pliki nie ma poświadczeń ani ruchu wychodzącego. Komunikaty błędów parsera nie trafiają do odpowiedzi. Ruch wychodzący z parsera jest monitorowany. Konfiguracja parsera jest objęta przeglądem kodu i testem regresji.
Wniosek
XXE nie zniknęło razem z modą na XML, ponieważ XML nie zniknął — tylko przestał być widoczny. Największym ryzykiem nie jest dziś endpoint przyjmujący dokument, lecz funkcja, o której nikt nie myśli jak o parserze: import arkusza, miniatura grafiki, weryfikacja tokenu logowania. Naprawa jest tania i trwała, bo jest konfiguracyjna: wyłącz definicje typu dokumentu, ustaw limity, odetnij parserowi sieć. Trudna część to inwentaryzacja — znalezienie wszystkich miejsc, w których aplikacja czyta XML, choć nikt tego tak nie nazwał.
Źródła pierwotne: OWASP — XML External Entity Prevention Cheat Sheet, CWE-611: Improper Restriction of XML External Entity Reference, CWE-776: XML Entity Expansion.


