Krótko: XXE w PHP zapobiegasz, ładując XML z obcych źródeł bez opcji LIBXML_NOENT, LIBXML_DTDLOAD i LIBXML_DTDVALID, odrzucając dokumenty z własnym DTD i ustawiając entity loader, który odmawia zewnętrznych zasobów. Od libxml 2.9.0 PHP domyślnie nie zastępuje już encji, dlatego libxml_disable_entity_loader() jest od PHP 8.0 przestarzała. Od PHP 8.4 z libxml 2.13 dostępna jest dodatkowo LIBXML_NO_XXE.
Stara luka, która nie znika
XML External Entities, w skrócie XXE, uchodzą u wielu programistów za temat załatwiony. PHP wyłączyło domyślnie ładowanie zewnętrznych encji we współczesnych wersjach libxml, a dawna funkcja ochronna została oznaczona w PHP 8.0 jako przestarzała. Mimo to luki XXE nadal pojawiają się podczas kontroli.
Przyczyną jest prawie zawsze jedna z trzech sytuacji: ktoś ustawił opcję, aby rozwiązać problem z importem, i tym samym ponownie włączył zastępowanie encji. Biblioteka robi to wewnętrznie. Albo aplikacja działa na starym systemie, którego PHP zbudowano z bardzo starą libxml.
XXE jest niebezpieczne, ponieważ parser XML doładowuje wtedy treści, do których dokument tylko się odwołuje: lokalne pliki lub adresy w sieci. Możliwe skutki to odczyt plików konfiguracyjnych, żądania do systemów wewnętrznych podobnie jak w SSRF oraz przeciążenie serwera.
Co obowiązuje od libxml 2.9 i PHP 8.0
Podręcznik PHP opisuje sytuację tak: od libxml 2.9.0 zastępowanie encji jest domyślnie wyłączone. Zewnętrznych encji nie trzeba więc osobno wyłączać, chyba że wewnętrzne odwołania do encji mają być rozwiązywane za pomocą określonych opcji. Z tego wynika poniższe zestawienie:
| Opcja | Działanie | Ryzyko przy obcym XML |
|---|---|---|
LIBXML_NOENT |
zastępuje encje | według podręcznika może ułatwiać ataki XXE |
LIBXML_DTDLOAD |
ładuje zewnętrzny podzbiór | włącza ładowanie zewnętrznych encji |
LIBXML_DTDVALID |
waliduje względem DTD | może ułatwiać ataki XXE |
LIBXML_NONET |
zabrania dostępu sieciowego przy ładowaniu | dodatkowa opcja ochronna |
LIBXML_NO_XXE |
wyłącza zewnętrzne encje przy zastępowaniu | ochronna, od PHP 8.4 i libxml 2.13.0 |
LIBXML_PARSEHUGE |
znosi wewnętrzne limity parsera | ułatwia przeciążenie, tylko dla zaufanych danych |
Mylna nazwa LIBXML_NOENT to znana pułapka: brzmi jak „bez encji”, a powoduje coś odwrotnego.
Dla programistów oznacza to: ryzykowne nie jest samo ładowanie XML, lecz każde miejsce, które świadomie zmienia zachowanie domyślne. Wyszukanie tych stałych w kodzie jest dlatego najszybszą drogą do wstępnej oceny, jeszcze przed jakąkolwiek pracochłonną kontrolą.
Skuteczna obrona przed XXE w PHP: wzorzec dla DOMDocument
Poniższy przykład łączy kilka warstw ochrony. Ustawia entity loader, który odmawia każdego zewnętrznego zasobu, ładuje bez ryzykownych opcji i odrzuca dokumenty z własnym DTD:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Zasadniczo odmawiać zewnętrznych zasobów
libxml_set_external_entity_loader(
static fn (?string $public, string $system, array $context) => null
);
$optionen = LIBXML_NONET;
if (defined('LIBXML_NO_XXE')) {
$optionen |= LIBXML_NO_XXE;
}
$vorher = libxml_use_internal_errors(true);
$dom = new DOMDocument();
$ok = $dom->loadXML($xml, $optionen);
libxml_use_internal_errors($vorher);
if (!$ok) {
throw new InvalidArgumentException('XML ungültig.');
}
if ($dom->doctype !== null) {
throw new InvalidArgumentException('Dokumente mit DTD werden nicht angenommen.');
}
return $dom;
}
Według podręcznika PHP wartość zwracana null w loaderze blokuje ładowanie zewnętrznego zasobu. OWASP zwraca uwagę, że nie wystarczy przekazać null bezpośrednio do libxml_set_external_entity_loader(); potrzebna jest funkcja zwracająca null. Dla SimpleXML i XMLReader obowiązują te same opcje. Wszędzie tam, gdzie to możliwe, OWASP zaleca całkowite odrzucanie DTD.
Jeśli sami możecie określić format danych, JSON jest zwykle prostszym wyborem do wymiany między własnymi systemami. Dla obcych formatów, takich jak SAML czy pliki Office, XML pozostaje nieunikniony.
Znajdowanie ukrytych wejść XML
Wiele aplikacji przetwarza więcej XML, niż myślą ich programiści. Przejdź przez tę listę:
- Interfejsy: klienci i serwery SOAP, XML-RPC, usługi płatności i wysyłki z odpowiedziami XML.
- Logowanie: Single sign-on z SAML opiera się na dokumentach XML pochodzących z zewnątrz.
- Importy: dane produktów, kanały, mapy witryn, formaty katalogów.
- Uploady: obrazy SVG to XML; formaty Office, takie jak DOCX i XLSX, składają się wewnętrznie z plików XML.
- Biblioteki: pakiety Composera do dokumentów, arkuszy lub SAML, które wewnętrznie używają libxml.
W kodzie pomagają dwa wyszukiwania:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Znaleziska w vendor/ nie są powodem do paniki, ale są okazją do sprawdzenia wersji biblioteki i komunikatów bezpieczeństwa.
W przypadku uploadów SVG XXE jest tylko jednym z kilku ryzyk, ponieważ pliki SVG mogą zawierać także skrypty. Jeśli Państwa aplikacja nie potrzebuje tego formatu bezwzględnie, w ogóle nie dopuszczaj go do uploadów. W przeciwnym razie przetwarzaj SVG wyłącznie według bezpiecznego wzorca powyżej i dostarczaj pliki tak, aby przeglądarka nie wykonywała zawartych skryptów.
Zwróć też uwagę na komunikaty o błędach. Jeśli aplikacja zwraca użytkownikom komunikaty libxml bez filtrowania, mogą w nich znaleźć się fragmenty przetwarzanej treści. Błędy należą do logu, użytkownik otrzymuje ogólny komunikat; w powyższym przykładzie libxml_use_internal_errors(true) sprawia, że ostrzeżenia nie trafiają do wyjścia. Ogranicz ponadto rozmiar otrzymywanych dokumentów XML, zanim parser w ogóle je zobaczy.
Stare systemy: kiedy brakuje ochrony domyślnej
Ochrona wynikająca z zachowania domyślnego zależy od wersji libxml, z którą zbudowano PHP. To, jaka wersja działa, pokazuje php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' w wierszu poleceń albo wynik phpinfo() w chronionym środowisku. Sprawdź wartość dla każdej wersji PHP na serwerze, ponieważ CLI i serwer WWW mogą używać różnych buildów. Obrazy kontenerów przynoszą własną libxml; tam sprawdzaj w działającym kontenerze, a nie na hoście.
Szczególnie narażone są systemy, które i tak działają na starych wersjach PHP. Dla PHP 8.1 i starszych po PHP2Shell nie ma już oficjalnych aktualizacji projektu PHP; co to oznacza, opisuje artykuł PHP 8.1 i starsze: co teraz obowiązuje. Stary system z przestarzałą libxml i otwartymi lukami interpretera należy nie tylko zabezpieczyć w kodzie, ale zmigrować. Pokrewne ryzyka przy przetwarzaniu obcych danych opisuje artykuł Niebezpieczna deserializacja w PHP, a pobieranie celów wewnętrznych artykuł Zapobieganie Server-Side Request Forgery w PHP.
Jak konkretnie pomaga PHP SOS?
- Znajdujemy wszystkie miejsca, w których Państwa aplikacja i jej biblioteki przetwarzają XML z obcych źródeł.
- Usuwamy ryzykowne opcje parsera i wprowadzamy centralną, zabezpieczoną funkcję ładującą.
- Sprawdzamy wersje libxml i PHP na wszystkich serwerach i planujemy migrację starych systemów.
- Testujemy zmiany nieszkodliwymi dokumentami kontrolnymi i dokumentujemy wynik.
Kontrolę własnego oprogramowania opisuje usługa audyt bezpieczeństwa i test penetracyjny.
Częste pytania
Czy aplikacje PHP są od PHP 8 automatycznie chronione przed XXE?
W przypadku domyślnym w dużej mierze tak, ponieważ libxml od wersji 2.9.0 nie zastępuje już encji. Ochrona znika jednak, gdy tylko kod ustawia opcje takie jak LIBXML_NOENT, LIBXML_DTDLOAD lub LIBXML_DTDVALID albo gdy PHP zbudowano z bardzo starą wersją libxml.
Czy nadal powinienem wywoływać libxml_disable_entity_loader?
Nie. Funkcja jest od PHP 8.0 przestarzała, ponieważ ładowanie zewnętrznych encji jest domyślnie wyłączone. Dla dodatkowej ochrony podręcznik PHP zaleca libxml_set_external_entity_loader, a od PHP 8.4 z odpowiednią libxml stałą LIBXML_NO_XXE.
Gdzie moja aplikacja w ogóle przetwarza XML?
Często w nieoczekiwanych miejscach: interfejsy SOAP, single sign-on z SAML, kanały RSS, mapy witryn, importy produktów, uploady SVG i pliki Office, które wewnętrznie składają się z XML. Także biblioteki mogą przetwarzać XML, bez że jest to widoczne we własnym kodzie.
Czym jest atak Billion Laughs?
Dokument z zagnieżdżonymi encjami, który przy rozwiązywaniu ogromnie rośnie i zużywa pamięć oraz czas obliczeń. Należy do tej samej rodziny co XXE. Ochronę zapewniają wyłączenie zastępowania encji i odrzucanie dokumentów z własnym DTD.