W3docs

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_functioncallable или 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.

Практика

Практика
Какова основная цель функции PHP libxml_set_external_entity_loader()?
Какова основная цель функции PHP libxml_set_external_entity_loader()?
Was this page helpful?