Aplikacja, która przyjmuje XML od użytkownika, może niepostrzeżenie pozwolić mu zmienić strukturę dokumentu, odczytać dane albo wpłynąć na zapytanie XPath. Termin xml injection obejmuje kilka podobnych problemów, dlatego wyjaśniam, jak rozpoznać poszczególne warianty, jakie mogą być skutki i jak bezpiecznie obsługiwać XML w Pythonie. Pokazuję też praktyczne wzorce kodu, typowe błędy oraz sposób testowania zabezpieczeń.
Bezpieczna obsługa XML zaczyna się od kontroli danych i parsera
- Źródłem problemu jest najczęściej składanie XML lub XPath przez konkatenację danych użytkownika.
- XXE to osobny, szczególnie groźny wariant związany z zewnętrznymi encjami i deklaracjami DTD.
- ElementTree z bezpiecznym tworzeniem węzłów ogranicza ryzyko manipulacji strukturą dokumentu.
- defusedxml pomaga zabezpieczyć aplikacje Pythona przed wybranymi atakami na parsery XML.
- Walidacja schematu nie zastępuje kodowania danych, limitów zasobów i wyłączenia zewnętrznych odwołań.

Czym jest wstrzykiwanie XML i dlaczego łatwo pomylić je z XXE
Do podatności dochodzi wtedy, gdy aplikacja traktuje dane pochodzące od użytkownika jak fragment składni XML, zamiast jak zwykłą wartość. Atakujący może próbować dodać własne elementy, atrybuty, encje lub znaki zmieniające strukturę dokumentu. MITRE opisuje ten problem jako CWE-91, wskazując między innymi na niewłaściwe neutralizowanie znaków specjalnych XML.
Najprostszy błąd wygląda niewinnie. Program buduje dokument za pomocą f-stringa, operatora + albo metody .format(), a następnie przekazuje wynik do parsera. Jeżeli zmienna zawiera znaki takie jak <, > lub &, może przestać być tekstem i zacząć działać jak część dokumentu.
from xml.etree.ElementTree import fromstring
name = user_input
xml = f"{name} "
root = fromstring(xml)
Nie każde niepoprawne wejście od razu prowadzi do przejęcia systemu. Czasem aplikacja po prostu zwróci błąd parsowania. Problem staje się poważny, gdy ten sam mechanizm pozwala zmienić dane biznesowe, ominąć kontrolę uprawnień albo wpłynąć na zapytanie wykonywane na strukturze XML.
Trzy zjawiska, które trzeba rozróżniać
- Wstrzykiwanie do XML zmienia strukturę dokumentu, na przykład przez dodanie elementu lub atrybutu.
- XPath injection manipuluje zapytaniem XPath, gdy dane użytkownika są wstawiane bezpośrednio do jego składni.
- XXE wykorzystuje zewnętrzne encje lub DTD, aby parser odwołał się do plików, adresów sieciowych albo innych zasobów.
W praktyce te nazwy bywają używane zamiennie, ale sposób obrony nie zawsze jest taki sam. Kodowanie danych rozwiązuje problem składania XML, natomiast przed XXE trzeba dodatkowo wyłączyć DTD, encje zewnętrzne i dostęp sieciowy parsera.
Jak atakujący wykorzystuje błędne składanie dokumentu
Najczęstszy scenariusz zaczyna się od formularza, importu pliku, endpointu SOAP, integracji B2B albo pola konfiguracji. Aplikacja pobiera wartość, dokleja ją do szablonu XML i zakłada, że użytkownik podał wyłącznie prosty tekst. To założenie jest szczególnie ryzykowne w systemach, które przyjmują dane od wielu klientów lub z zewnętrznych integracji.
Przykładowo, pole opisujące produkt może zostać użyte przy tworzeniu dokumentu zamówienia. Jeśli opis jest wstawiany bezpośrednio do tekstu XML, znaki specjalne mogą zamknąć bieżący element i otworzyć kolejny. W efekcie parser otrzyma dokument inny niż ten, który projektant aplikacji miał na myśli.
Bezpieczniejszy wzorzec polega na tworzeniu struktury za pomocą API biblioteki, a nie przez ręczne łączenie tekstu. Biblioteka sama zakoduje znaki specjalne i potraktuje zawartość jako wartość elementu.
from xml.etree.ElementTree import Element, SubElement, tostring
user = Element("user")
name = SubElement(user, "name")
name.text = user_input
xml_bytes = tostring(user, encoding="utf-8")
To proste rozdzielenie składni od danych robi dużą różnicę. W mojej ocenie jest znacznie lepsze niż ręczne wywoływanie funkcji do zamiany znaków, ponieważ zmniejsza ryzyko pominięcia jednego miejsca w kodzie. Ręczne escapowanie bywa potrzebne w wyjątkowych przypadkach, lecz nie powinno być podstawową metodą budowania całego dokumentu.
Ryzyko związane z XPath
Drugi częsty błąd pojawia się przy wyszukiwaniu użytkownika po nazwie lub identyfikatorze. Kod tworzy zapytanie XPath jako zwykły napis, a dane wejściowe trafiają do niego bez kontroli. Wtedy użytkownik nie zmienia już tylko treści XML, lecz może próbować zmienić samą logikę warunku.
# ryzykowny wzorzec
query = f".//user[name='{user_input}']"
result = root.find(query)
W tym przypadku samo użycie ElementTree nie rozwiązuje problemu, bo błąd znajduje się w konstruowaniu zapytania. Najbezpieczniej używać identyfikatorów o określonym typie, na przykład liczby całkowitej, albo wyszukiwać rekordy po wartościach w kodzie Pythona. Jeśli aplikacja musi korzystać z XPath, trzeba użyć mechanizmu parametrów dostępnego w wybranej bibliotece i ściśle ograniczyć dozwolone dane.
Jakie skutki może mieć podatność
Skutek zależy od tego, gdzie XML jest przetwarzany i jakie uprawnienia ma proces aplikacji. W prostym formularzu problem może skończyć się błędem lub modyfikacją pojedynczego rekordu. W systemie integracyjnym ta sama klasa błędu może doprowadzić do ujawnienia danych, SSRF albo odmowy działania usługi.
| Wariant problemu | Możliwy skutek | Co zwiększa ryzyko |
|---|---|---|
| Wstrzyknięcie elementu lub atrybutu | Zmiana danych, obejście reguł biznesowych, błędy autoryzacji | Brak walidacji struktury i ręczne składanie XML |
| XPath injection | Odczyt nieprzewidzianych węzłów lub zmiana warunku wyszukiwania | Łączenie danych użytkownika z zapytaniem XPath |
| XXE | Odczyt plików, żądania do sieci wewnętrznej, skanowanie usług | Aktywne DTD i zewnętrzne encje |
| Rozbudowane encje | Zużycie pamięci i procesora, niedostępność aplikacji | Brak limitów rozmiaru, głębokości i ekspansji |
XXE nie jest po prostu inną nazwą każdego ataku na XML. To konkretny wariant, w którym parser może pobrać zasób wskazany przez dokument. OWASP zaleca wyłączenie DTD i zewnętrznych encji, ponieważ ich obsługa rzadko jest potrzebna w typowych API, a może otworzyć drogę do odczytu plików lokalnych i żądań wychodzących z serwera.
Duże znaczenie ma także konto systemowe procesu. Nawet dobrze skonfigurowany parser nie powinien działać z uprawnieniami administratora ani mieć nieograniczonego dostępu do sieci. To warstwa awaryjna, która ogranicza skutki błędu, ale nie zastępuje poprawnej ochrony kodu.
Jak zabezpieczyć XML w Pythonie
Obronę warto budować na kilku poziomach. Najpierw oddzielam dane od składni, później używam bezpiecznie skonfigurowanego parsera, a na końcu ograniczam rozmiar wejścia, czas przetwarzania i dostęp procesu do zasobów. Pojedyncza kontrola, na przykład sprawdzenie, czy tekst zawiera napis , jest zbyt krucha i łatwo ją ominąć.
Twórz strukturę przez API biblioteki
Do generowania XML używaj obiektów elementów oraz atrybutów, zamiast szablonów tekstowych. Wartości przypisuj przez .text lub .set(). Biblioteka zajmie się kodowaniem znaków, a kod staje się łatwiejszy do testowania.
from xml.etree.ElementTree import Element, SubElement, tostring
order = Element("order")
order.set("id", str(order_id))
comment = SubElement(order, "comment")
comment.text = comment_from_user
payload = tostring(order, encoding="utf-8", xml_declaration=True)
Jeżeli XML ma być tylko formatem komunikacji, rozważ JSON. Nie rozwiąże on wszystkich problemów bezpieczeństwa, ale często upraszcza walidację i usuwa ryzyka związane z DTD, encjami oraz złożoną składnią XML. Wybór XML ma sens wtedy, gdy wymagają go istniejące systemy, schematy lub standardy branżowe.
Używaj parsera odpornego na typowe ataki
W przypadku nieufnego XML warto rozważyć pakiet defusedxml, który dostarcza bezpieczniejsze opakowania dla popularnych modułów standardowej biblioteki. Przykład obsługi powinien również przewidywać błędy parsowania i odrzucać zbyt duże dane przed przekazaniem ich parserowi.
from defusedxml.ElementTree import fromstring
from xml.etree.ElementTree import ParseError
try:
if len(xml_bytes) > 1_000_000:
raise ValueError("Dokument jest zbyt duży")
root = fromstring(xml_bytes)
except (ParseError, ValueError):
raise ValueError("Nieprawidłowy dokument XML")
Limit jednego megabajta nie jest uniwersalną regułą. Dla małej wiadomości może być odpowiedni, dla dokumentów branżowych będzie zbyt niski. Najlepiej ustalić go na podstawie rzeczywistego formatu, dodać limit liczby elementów i monitorować czas parsowania.
Skonfiguruj lxml świadomie
Jeśli projekt używa biblioteki lxml, parser powinien mieć wyłączone ładowanie DTD, rozwiązywanie encji oraz połączenia sieciowe. Konkretne opcje trzeba sprawdzić dla używanej wersji biblioteki, ale podstawowy wzorzec wygląda tak:
from lxml import etree
parser = etree.XMLParser(
load_dtd=False,
resolve_entities=False,
no_network=True,
huge_tree=False
)
root = etree.fromstring(xml_bytes, parser=parser)
Nie włączaj trybu huge_tree tylko dlatego, że parser zgłasza ograniczenie rozmiaru. Ta opcja może wyłączyć część zabezpieczeń przed bardzo dużymi dokumentami. Jeśli duży XML jest wymagany, lepiej ustalić kontrolowany limit, przetwarzać dane strumieniowo i testować zachowanie pod obciążeniem.
Waliduj dane biznesowo
Poprawna składnia XML nie oznacza, że dane są bezpieczne dla aplikacji. Schemat XSD może sprawdzić typy, wymagane pola i dozwolone elementy, ale nie powinien być jedyną ochroną. Walidacja musi obejmować także zakresy wartości, długości pól, liczbę elementów i reguły uprawnień.
Jeżeli proces walidacji korzysta z zewnętrznego schematu, upewnij się, że parser nie pobiera go automatycznie z Internetu. Najbezpieczniej przechowywać wymagane schematy lokalnie, kontrolować ich wersje i wyłączyć wszelkie niepotrzebne odwołania sieciowe.
Jak testować aplikację przed wdrożeniem
Test zaczynam od przeglądu miejsc, w których XML jest tworzony lub parsowany. Szukam konkatenacji, f-stringów, szablonów, dynamicznych wyrażeń XPath oraz ustawień parsera zmienianych w kilku różnych modułach. Szczególnie dokładnie sprawdzam importy plików, webhooki i endpointy, które przyjmują nagłówek Content-Type: application/xml.
Testy jednostkowe powinny obejmować zwykłe dane graniczne, takie jak znaki <, >, &, cudzysłowy, bardzo długie teksty, puste wartości i nieoczekiwane zagnieżdżenie. W kontrolowanym środowisku testowym sprawdzam również, czy dokument zawierający deklarację DTD lub encję zewnętrzną jest odrzucany, a aplikacja nie wykonuje połączeń do sieci ani nie odczytuje plików.
Przeczytaj również: Koa.js - Czy to framework dla Twojego backendu? Sprawdź!
Na co zwrócić uwagę w wynikach
- Brak dostępu do sieci podczas parsowania nieufnego dokumentu.
- Brak odczytu plików przez mechanizmy encji i XInclude.
- Kontrolowany czas odpowiedzi dla dokumentów o dużej głębokości.
- Jednolity błąd walidacji, który nie ujawnia ścieżek plików ani konfiguracji serwera.
- Odrzucanie nieznanych elementów, jeśli aplikacja nie potrzebuje trybu rozszerzalnego.
W testach dynamicznych korzystaj wyłącznie z własnej aplikacji lub środowiska, na które masz zgodę. Narzędzia DAST i SAST mogą wskazać ryzykowne miejsca, lecz nie zastąpią ręcznego sprawdzenia konfiguracji parsera. Skaner może wykryć podejrzane składanie XPath, ale nie zawsze rozpozna, że schemat walidacji pozwala na zewnętrzne odwołania.
Po wdrożeniu monitoruję błędy parsowania, nietypowe rozmiary dokumentów i nagłe wzrosty czasu obsługi. Same logi nie powinny zawierać całego nieufnego XML, ponieważ może on zawierać dane osobowe, sekrety albo złośliwe fragmenty. Rejestruj raczej typ błędu, identyfikator żądania i bezpiecznie skrócony kontekst.
Najkrótsza droga do bezpiecznego parsera XML w Pythonie
Jeśli XML jest konieczny, zacznij od dwóch decyzji. Nie składaj dokumentów przez łączenie napisów i nie parsuj nieufnego wejścia na domyślnych ustawieniach bez sprawdzenia obsługi DTD, encji oraz dostępu sieciowego. W Pythonie praktyczny zestaw startowy to tworzenie elementów przez API, parser odporny na ataki, walidacja biznesowa i limity zasobów.
Najwięcej błędów powstaje nie w samym XML, lecz na granicy między danymi użytkownika a składnią programu. Gdy ta granica jest wyraźna, parser ma ograniczone możliwości, a proces działa z minimalnymi uprawnieniami, ryzyko spada bardzo wyraźnie. Taki zestaw nie komplikuje aplikacji, a przede wszystkim chroni ją przed problemem, który często zaczyna się od jednej niewinnej konkatenacji tekstu.
