Накратко: XXE в PHP предотвратявате, като зареждате XML от чужди източници без опциите LIBXML_NOENT, LIBXML_DTDLOAD и LIBXML_DTDVALID, отхвърляте документи със собствена DTD и задавате Entity Loader, който отказва външни ресурси. От libxml 2.9.0 PHP вече не замества entities по подразбиране, затова libxml_disable_entity_loader() е остаряла от PHP 8.0. От PHP 8.4 с libxml 2.13 допълнително е налична LIBXML_NO_XXE.
Стара уязвимост, която не изчезва
XML External Entities, накратко XXE, за много разработчици са приключена тема. PHP изключи зареждането на външни entities по подразбиране при съвременни версии на libxml, а старата защитна функция беше маркирана като остаряла с PHP 8.0. Въпреки това XXE уязвимости продължават да се появяват при проверки.
Причината почти винаги е в една от три ситуации: някой е задал опция, за да реши проблем с импорт, и така е включил отново замяната на entities. Библиотека го прави вътрешно. Или приложението работи на стара система, чийто PHP е изграден с много стара libxml.
XXE е опасна, защото XML парсерът при това зарежда съдържание, което в документа е само реферирано: локални файлове или адреси в мрежата. Възможни последици са прочитане на конфигурационни файлове, заявки към вътрешни системи подобно на SSRF и претоварване на сървъра.
Какво важи от libxml 2.9 и PHP 8.0
Наръчникът на PHP описва положението така: от libxml 2.9.0 замяната на entities е деактивирана по подразбиране. Външните entities затова вече не се налага да се изключват отделно, освен ако вътрешни препратки към entities трябва да се разрешават с определени опции. От това следва този преглед:
| Опция | Действие | Риск при чужд XML |
|---|---|---|
LIBXML_NOENT |
замества entities | според наръчника може да улесни XXE атаки |
LIBXML_DTDLOAD |
зарежда външното подмножество | активира зареждането на външни entities |
LIBXML_DTDVALID |
валидира спрямо DTD | може да улесни XXE атаки |
LIBXML_NONET |
забранява мрежови достъпи при зареждане | допълнителна защитна опция |
LIBXML_NO_XXE |
изключва външните entities при замяната | защитна, от PHP 8.4 и libxml 2.13.0 |
LIBXML_PARSEHUGE |
премахва вътрешните ограничения на парсера | улеснява претоварване, само за надеждни данни |
Подвеждащото име на LIBXML_NOENT е позната клопка: звучи като „без entities“, но има обратен ефект.
За разработчиците това означава: рисково не е самото зареждане на XML, а всяко място, което съзнателно променя стандартното поведение. Търсенето на тези константи в кода затова е най-бързият път към първа оценка, още преди всяка трудоемка проверка.
Сигурна защита от XXE в PHP: образец за DOMDocument
Следващият пример комбинира няколко защитни слоя. Той задава Entity Loader, който отказва всеки външен ресурс, зарежда без рискови опции и отхвърля документи със собствена DTD:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// По принцип отказвайте външните ресурси
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;
}
Според наръчника на PHP върната стойност null в Loader-а блокира зареждането на външния ресурс. OWASP посочва, че не е достатъчно да се предаде null директно на libxml_set_external_entity_loader(); нужна е функция, която връща null. За SimpleXML и XMLReader важат същите опции. Където е възможно, OWASP препоръчва DTD да се отхвърлят изцяло.
Ако можете сами да определите формата на данните, JSON за обмен между собствени системи обикновено е по-простият избор. За чужди формати като SAML или офис файлове XML остава неизбежен.
Откриване на скритите входове за XML
Много приложения обработват повече XML, отколкото предполагат разработчиците им. Преминете през този списък:
- Интерфейси: SOAP клиенти и сървъри, XML-RPC, платежни и куриерски услуги с XML отговори.
- Вход в системата: Single Sign-on със SAML се основава на XML документи, които идват отвън.
- Импорти: продуктови данни, емисии (feeds), sitemap-ове, формати на каталози.
- Качвания: SVG изображенията са XML; офис формати като DOCX и XLSX вътрешно се състоят от XML файлове.
- Библиотеки: Composer пакети за документи, таблици или SAML, които вътрешно ползват libxml.
В кода помагат две търсения:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Намерени места във vendor/ не са повод за паника, но са причина да проверите версията и съобщенията за сигурност на библиотеката.
При SVG качванията XXE е само един от няколко риска, защото SVG файловете могат да съдържат и скриптове. Ако Вашето приложение не се нуждае непременно от формата, изобщо не го допускайте за качвания. В противен случай обработвайте SVG само със сигурния образец по-горе и доставяйте файловете така, че браузърът да не изпълнява съдържащите се скриптове.
Обърнете внимание и на съобщенията за грешка. Ако едно приложение връща съобщенията на libxml нефилтрирани на потребителите, в тях могат да се съдържат части от обработваното съдържание. Грешките принадлежат в журнала, потребителят получава общо съобщение; в примера по-горе libxml_use_internal_errors(true) гарантира, че предупрежденията не попадат в изхода. Ограничете освен това размера на получаваните XML документи, преди парсерът изобщо да ги види.
Стари системи: кога липсва стандартната защита
Защитата чрез стандартното поведение зависи от версията на libxml, с която е изграден PHP. Коя версия работи, показва php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' в командния ред или изходът на phpinfo() в защитена среда. Проверете стойността за всяка версия на PHP на сървъра, защото CLI и уеб сървърът могат да ползват различни изграждания. Образите на контейнери носят собствена libxml; там проверявайте в работещия контейнер, а не на хоста.
Особено засегнати са системите, които така или иначе работят на стари версии на PHP. За PHP 8.1 и по-стари след PHP2Shell няма повече официални актуализации от проекта PHP; какво означава това, описва PHP 8.1 и по-стари: какво важи сега. Стара система с остаряла libxml и отворени уязвимости в интерпретатора трябва не само да бъде защитена в кода, а да бъде мигрирана. Сродни рискове при обработката на чужди данни описва Несигурна десериализация в PHP, а извикванията на вътрешни цели Предотвратяване на Server-Side Request Forgery в PHP.
Как помага PHP SOS конкретно?
- Намираме всички места, където Вашето приложение и библиотеките му обработват XML от чужди източници.
- Премахваме рисковите опции на парсера и въвеждаме централна, защитена функция за зареждане.
- Проверяваме версиите на libxml и PHP на всички сървъри и при стари системи планираме миграцията.
- Тестваме промените с безвредни тестови документи и документираме резултата.
Проверката на собствен софтуер описва услугата Одит на сигурността и тест за проникване.
Често задавани въпроси
Защитени ли са PHP приложенията автоматично от XXE от PHP 8 насам?
В стандартния случай до голяма степен, защото libxml от версия 2.9.0 вече не замества entities. Защитата отпада обаче, щом кодът зададе опции като LIBXML_NOENT, LIBXML_DTDLOAD или LIBXML_DTDVALID или PHP е изграден с много стара версия на libxml.
Трябва ли още да извиквам libxml_disable_entity_loader?
Не. Функцията е остаряла от PHP 8.0, защото зареждането на външни entities е изключено по подразбиране. За допълнителна защита наръчникът на PHP препоръчва libxml_set_external_entity_loader, а от PHP 8.4 с подходяща libxml и константата LIBXML_NO_XXE.
Къде изобщо моето приложение обработва XML?
Често на неочаквани места: SOAP интерфейси, Single Sign-on със SAML, RSS емисии, sitemap-ове, импорти на продукти, SVG качвания и офис файлове, които вътрешно се състоят от XML. И библиотеките могат да обработват XML, без това да е видимо в собствения код.
Какво е атаката Billion Laughs?
Документ с вложени entities, който при разрешаване нараства огромно и изразходва памет и изчислително време. Тя принадлежи към същото семейство като XXE. Защита дават изключването на замяната на entities и отхвърлянето на документи със собствена DTD.