En resumen: para evitar XXE en PHP, cargue el XML de fuentes externas sin las opciones LIBXML_NOENT, LIBXML_DTDLOAD y LIBXML_DTDVALID, rechace los documentos con DTD propia y establezca un cargador de entidades que deniegue los recursos externos. Desde libxml 2.9.0, PHP ya no sustituye entidades por defecto, por lo que libxml_disable_entity_loader() está obsoleta desde PHP 8.0. Desde PHP 8.4 con libxml 2.13 se dispone además de LIBXML_NO_XXE.
Una vulnerabilidad antigua que no desaparece
Para muchos desarrolladores, las entidades externas XML, abreviado XXE, son un tema resuelto. Con las versiones modernas de libxml, PHP ha desactivado por defecto la carga de entidades externas, y la antigua función de protección se marcó como obsoleta con PHP 8.0. Aun así, las vulnerabilidades XXE siguen apareciendo en las auditorías.
La razón casi siempre está en una de tres situaciones: alguien estableció una opción para resolver un problema de importación y con ello volvió a activar la sustitución de entidades. Una biblioteca lo hace internamente. O la aplicación se ejecuta en un sistema heredado cuyo PHP se compiló con una libxml muy antigua.
XXE es peligroso porque el analizador XML carga contenidos que en el documento solo están referenciados: archivos locales o direcciones de la red. Las posibles consecuencias son la lectura de archivos de configuración, peticiones a sistemas internos de forma similar a SSRF y la sobrecarga del servidor.
Qué rige desde libxml 2.9 y PHP 8.0
El manual de PHP describe la situación así: desde libxml 2.9.0, la sustitución de entidades está desactivada por defecto. Por tanto, las entidades externas ya no tienen que desactivarse expresamente, salvo que se quieran resolver referencias de entidades internas con determinadas opciones. De ello resulta esta visión general:
| Opción | Efecto | Riesgo con XML ajeno |
|---|---|---|
LIBXML_NOENT |
sustituye entidades | según el manual, puede facilitar ataques XXE |
LIBXML_DTDLOAD |
carga el subconjunto externo | activa la carga de entidades externas |
LIBXML_DTDVALID |
valida frente a la DTD | puede facilitar ataques XXE |
LIBXML_NONET |
prohíbe accesos de red durante la carga | opción adicional de protección |
LIBXML_NO_XXE |
desactiva las entidades externas en la sustitución | protectora, desde PHP 8.4 y libxml 2.13.0 |
LIBXML_PARSEHUGE |
elimina los límites internos del analizador | facilita la sobrecarga, solo para datos de confianza |
El nombre engañoso de LIBXML_NOENT es una trampa conocida: suena a «sin entidades», pero hace lo contrario.
Para los desarrolladores esto significa: lo arriesgado no es cargar XML en sí, sino cualquier punto que cambie deliberadamente el comportamiento predeterminado. Una búsqueda de estas constantes en el código es, por tanto, el camino más rápido para una primera valoración, antes de cualquier revisión laboriosa.
Defenderse de XXE en PHP con seguridad: un patrón para DOMDocument
El ejemplo siguiente combina varias capas de protección. Establece un cargador de entidades que deniega cualquier recurso externo, carga sin opciones arriesgadas y rechaza los documentos con DTD propia:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Denegar por principio los recursos externos
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;
}
Según el manual de PHP, un valor de retorno null en el cargador bloquea la carga del recurso externo. OWASP señala que no basta con pasar null directamente a libxml_set_external_entity_loader(); hace falta una función que devuelva null. Para SimpleXML y XMLReader rigen las mismas opciones. Siempre que sea posible, OWASP recomienda rechazar las DTD por completo.
Si puede determinar usted mismo el formato de datos, JSON suele ser la opción más sencilla para el intercambio entre sistemas propios. Para formatos ajenos como SAML o archivos de Office, XML sigue siendo inevitable.
Encontrar las entradas XML ocultas
Muchas aplicaciones procesan más XML de lo que piensan sus desarrolladores. Repase esta lista:
- Interfaces: clientes y servidores SOAP, XML-RPC, servicios de pago y de envío con respuestas XML.
- Inicio de sesión: el inicio de sesión único con SAML se basa en documentos XML que llegan del exterior.
- Importaciones: datos de productos, feeds, sitemaps, formatos de catálogo.
- Subidas: las imágenes SVG son XML; los formatos de Office como DOCX y XLSX están formados internamente por archivos XML.
- Bibliotecas: paquetes de Composer para documentos, hojas de cálculo o SAML que usan libxml internamente.
En el código ayudan dos búsquedas:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Los hallazgos en vendor/ no son motivo de pánico, pero sí una razón para revisar la versión y los avisos de seguridad de la biblioteca.
En las subidas de SVG, XXE es solo uno de varios riesgos, porque los archivos SVG pueden contener también scripts. Si su aplicación no necesita imperiosamente el formato, no lo permita en absoluto para subidas. De lo contrario, procese SVG solo con el patrón seguro de arriba y entregue los archivos de modo que el navegador no ejecute los scripts contenidos.
Preste atención además a los mensajes de error. Si una aplicación devuelve sin filtrar a los usuarios los mensajes de libxml, en ellos pueden aparecer partes del contenido procesado. Los errores pertenecen al log; el usuario recibe un mensaje general; en el ejemplo anterior, libxml_use_internal_errors(true) se encarga de que las advertencias no lleguen a la salida. Limite además el tamaño de los documentos XML recibidos antes de que el analizador siquiera los vea.
Sistemas heredados: cuándo falta la protección estándar
La protección que da el comportamiento predeterminado depende de la versión de libxml con la que se compiló PHP. Qué versión se ejecuta lo muestran php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' en la línea de comandos o la salida de phpinfo() en un entorno protegido. Compruebe el valor para cada versión de PHP del servidor, porque CLI y servidor web pueden usar compilaciones distintas. Las imágenes de contenedor traen su propia libxml; allí compruebe en el contenedor en ejecución, no en el host.
Están especialmente afectados los sistemas que de todos modos se ejecutan en versiones antiguas de PHP. Para PHP 8.1 y anteriores no hay, tras PHP2Shell, más actualizaciones oficiales del proyecto PHP; qué significa eso lo describe PHP 8.1 y anteriores: qué rige ahora. Un sistema heredado con una libxml obsoleta y vulnerabilidades abiertas en el intérprete no debería solo protegerse en el código, sino migrarse. Riesgos relacionados en el procesamiento de datos ajenos los describe Deserialización insegura en PHP, y las consultas a destinos internos, Evitar el Server-Side Request Forgery en PHP.
¿Cómo ayuda PHP SOS en concreto?
- Encontramos todos los puntos en los que su aplicación y sus bibliotecas procesan XML de fuentes externas.
- Eliminamos las opciones de analizador arriesgadas e incorporamos una función de carga central y protegida.
- Comprobamos las versiones de libxml y de PHP en todos los servidores y, en los sistemas heredados, planificamos la migración.
- Probamos los cambios con documentos de prueba inofensivos y documentamos el resultado.
La revisión de software propio la describe el servicio Auditoría de seguridad y prueba de penetración.
Preguntas frecuentes
¿Están las aplicaciones PHP automáticamente protegidas frente a XXE desde PHP 8?
En el caso estándar, en gran medida, porque libxml ya no sustituye entidades desde la versión 2.9.0. Pero la protección desaparece en cuanto el código establece opciones como LIBXML_NOENT, LIBXML_DTDLOAD o LIBXML_DTDVALID, o si PHP se ha compilado con una versión de libxml muy antigua.
¿Debo seguir llamando a libxml_disable_entity_loader?
No. La función está obsoleta desde PHP 8.0, porque la carga de entidades externas está desactivada por defecto. Para una protección adicional, el manual de PHP recomienda libxml_set_external_entity_loader y, desde PHP 8.4 con una libxml adecuada, la constante LIBXML_NO_XXE.
¿Dónde procesa XML mi aplicación realmente?
A menudo en lugares inesperados: interfaces SOAP, inicio de sesión único con SAML, feeds RSS, sitemaps, importaciones de productos, subidas de SVG y archivos de Office, que internamente están formados por XML. También las bibliotecas pueden procesar XML sin que sea visible en el propio código.
¿Qué es el ataque Billion Laughs?
Un documento con entidades anidadas que, al resolverse, crece enormemente y consume memoria y tiempo de cálculo. Pertenece a la misma familia que XXE. La protección consiste en desactivar la sustitución de entidades y rechazar documentos con DTD propia.