libxml_set_external_entity_loader()
Разбираем функцию libxml_set_external_entity_loader() в PHP: регистрация callback для управления загрузкой внешних сущностей XML и защита от XXE.
Функция libxml_set_external_entity_loader() регистрирует пользовательский callback, который PHP-парсеры на основе libxml (DOMDocument, SimpleXML, XMLReader) вызывают каждый раз, когда XML-документ пытается загрузить внешнюю сущность — файл, URL или DTD, на который ссылается разметка. Управляя этим callback, вы сами решаете, какие внешние ресурсы разрешены к загрузке, а какие заблокированы. Это стандартный и перспективный способ защиты от атак XML External Entity (XXE). Данная страница объясняет, что такое внешние сущности, почему они опасны, описывает сигнатуру функции в PHP 8 и показывает, как написать безопасный загрузчик на рабочих примерах.
Что такое внешняя сущность (и почему XXE опасна)?
Внешняя сущность — это заполнитель, объявленный в определении типа документа (DTD), который ссылается на содержимое за пределами XML-документа. Например:
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY secret SYSTEM "file:///etc/passwd">
]>
<data>&secret;</data>Когда парсер разворачивает &secret;, он читает /etc/passwd и вставляет содержимое файла в документ. Злоумышленник, контролирующий XML-ввод, может таким образом читать локальные файлы, обращаться к внутренним сетевым эндпоинтам (SSRF) или инициировать атаку типа «billion laughs» (отказ в обслуживании). Блокировка или строгое разрешение загрузки внешних сущностей полностью исключает эту угрозу.
Что такое функция libxml_set_external_entity_loader()?
libxml_set_external_entity_loader() — встроенная PHP-функция, которая регистрирует единственный глобальный callback. С момента его установки каждый запрос внешней сущности от любого libxml-парсера сначала проходит через ваш callback. Возврат null сообщает libxml пропустить сущность; возврат содержимого сущности (в виде строки или открытого ресурса) разрешает её загрузку. Функция появилась в PHP 5.1.0 и по-прежнему является рекомендованным подходом, поскольку старая libxml_disable_entity_loader() устарела в PHP 8.0 и по большей части не нужна в современном PHP (загрузка внешних сущностей отключена по умолчанию начиная с libxml 2.9 / PHP 8.0).
Синтаксис
libxml_set_external_entity_loader(?callable $resolver_function): voidПередача null удаляет ранее зарегистрированный загрузчик и восстанавливает поведение по умолчанию.
Параметры
| Параметр | Описание |
|---|---|
resolver_function | callable или null для сброса. Callback принимает три аргумента и должен возвращать содержимое сущности (string), открытый resource или null для блокировки загрузки. |
Сигнатура callback в PHP 8.0+:
function (?string $public_id, ?string $system_id, array $context): string|resource|null$public_id— публичный идентификатор сущности (частоnull).$system_id— URI/путь, на который указывает сущность (например,file:///etc/passwd).$context— массив с ключамиdirectory,intSubName,extSubURIиextSubSystem, описывающими контекст разбора.
Возвращаемое значение
Сама libxml_set_external_entity_loader() возвращает void (до PHP 8.0 возвращала true).
Как использовать libxml_set_external_entity_loader()
Определите callback, зарегистрируйте его, затем выполняйте разбор. Самая простая безопасная политика — блокировать всё, всегда возвращая null:
<?php
// Block every external entity request.
libxml_set_external_entity_loader(
static fn (?string $publicId, ?string $systemId, array $context): ?string => null
);
$xml = <<<'XML'
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY secret SYSTEM "file:///etc/passwd">
]>
<data>&secret;</data>
XML;
$doc = new DOMDocument();
$doc->loadXML($xml);
// The entity was blocked, so it expands to nothing.
echo "Loaded value: [" . $doc->documentElement->textContent . "]\n";
echo "Done without reading any external file.\n";
?>Ссылка &secret; разворачивается в пустую строку, потому что наш загрузчик вернул null, и файл /etc/passwd никогда не читается.
Разрешение доверенных источников
Если приложению действительно нужны некоторые внешние сущности (например, локальный DTD, входящий в состав вашего кода), разрешите только те пути, которым вы доверяете, и отклоните всё остальное:
<?php
function trusted_entity_loader(?string $publicId, ?string $systemId, array $context)
{
// Only allow files inside our own schema directory.
$allowedDir = __DIR__ . '/schemas/';
if ($systemId === null) {
return null;
}
$path = str_starts_with($systemId, 'file://')
? substr($systemId, 7)
: $systemId;
$real = realpath($path);
if ($real !== false && str_starts_with($real, realpath($allowedDir))) {
return file_get_contents($real); // Trusted: load it.
}
return null; // Everything else is blocked.
}
libxml_set_external_entity_loader('trusted_entity_loader');
echo "Loader registered: only ./schemas/ files may be resolved.\n";
?>Здесь любой systemId, не разрешающийся в реальный файл внутри schemas/, возвращает null и блокируется, тогда как доверенные локальные файлы схем загружаются в обычном режиме.
Примечание: Загрузчик глобальный и применяется ко всем libxml-разборам в рамках запроса. Сбросьте его с помощью
libxml_set_external_entity_loader(null)по завершении работы, если другой код в том же запросе рассчитывает на поведение по умолчанию.
Распространённые ошибки и нюансы
- Возврат значения неверного типа. Возвращайте
string, открытыйresourceилиnull— возвратfalseилиtrueнедопустим и может вызватьTypeErrorв PHP 8. - Забытая глобальность. Последний зарегистрированный загрузчик действует для всего запроса; библиотеки, устанавливающие собственный загрузчик, могут переопределить ваш.
- Ложное убеждение, что
libxml_disable_entity_loader()всё ещё нужна. В PHP 8+ внешние сущности отключены по умолчанию; используйте эту функцию только для настраиваемого управления загрузкой. - Слепое доверие к
$systemId. Всегда проверяйте путь/URL перед чтением, иначе вы снова открываете уязвимость XXE/SSRF, от которой пытались защититься.
Заключение
libxml_set_external_entity_loader() предоставляет единую точку контроля для каждой внешней сущности, которую пытается загрузить libxml-парсер, делая эту функцию современным рекомендованным способом защиты от XXE и SSRF при обработке XML в PHP. Блокируйте всё, возвращая null, или разрешайте только те локальные ресурсы, которым доверяете. Обзор смежных инструментов libxml смотрите в связанных функциях ниже.
Смотрите также
- PHP libxml — обзор расширения libxml и его констант.
- libxml_disable_entity_loader() — устаревший способ отключить загрузку сущностей.
- PHP XML DOM — разбор XML с помощью
DOMDocument.