# Zapobieganie XML External Entities (XXE) w PHP

> Zapobieganie XXE w PHP: co obowiązuje od libxml 2.9 i PHP 8.0, które flagi są ryzykowne, LIBXML_NO_XXE od PHP 8.4 i jak znaleźć wejścia XML w aplikacji.

Źródło: https://phpsos.de/pl/blog/xxe-in-php/  
Stan: 25.10.2026  
Dostawca: PHP SOS, Pomoc awaryjna PHP i bezpieczeństwo serwerów. Natychmiastowa pomoc przez WhatsApp: https://wa.me/491736494914?text=Dzie%C5%84%20dobry%2C%20PHP%20SOS%2C%20potrzebujemy%20pomocy%20z%20naszym%20serwerem%20PHP.%20Firma%3A%20%E2%80%A6%20Strona%20internetowa%3A%20%E2%80%A6

**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
<?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.

> **Wskazówka:**
> 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ę:

1. **Interfejsy:** klienci i serwery SOAP, XML-RPC, usługi płatności i wysyłki z odpowiedziami XML.
2. **Logowanie:** Single sign-on z SAML opiera się na dokumentach XML pochodzących z zewnątrz.
3. **Importy:** dane produktów, kanały, mapy witryn, formaty katalogów.
4. **Uploady:** obrazy SVG to XML; formaty Office, takie jak DOCX i XLSX, składają się wewnętrznie z plików XML.
5. **Biblioteki:** pakiety Composera do dokumentów, arkuszy lub SAML, które wewnętrznie używają libxml.

W kodzie pomagają dwa wyszukiwania:

```bash
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](https://phpsos.de/pl/php2shell/php-8-1-und-aelter/). 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](https://phpsos.de/pl/ratgeber/deserialisierung-php/), a pobieranie celów wewnętrznych artykuł [Zapobieganie Server-Side Request Forgery w PHP](https://phpsos.de/pl/blog/ssrf-in-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](https://phpsos.de/pl/leistungen/sicherheitsaudit-penetrationstest/).


## 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.

## Źródła

- [Podręcznik PHP: libxml_disable_entity_loader](https://www.php.net/manual/de/function.libxml-disable-entity-loader.php)
- [Podręcznik PHP: stałe libxml](https://www.php.net/manual/de/libxml.constants.php)
- [OWASP XML External Entity Prevention Cheat Sheet (ściągawka o zapobieganiu XXE)](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html)
