XXE: útok přes externí entity v XML a jak se bránit

XXE (XML External Entity) je útok, při kterém podvržený XML dokument přinutí server přečíst vlastní soubor, zavolat vnitřní síť nebo se zahltit. Kapitola ukazuje, jak útok funguje, proč vzniká a jak ho vypnout v parserech PHP, .NET, Javy, Pythonu a Node.js.

Autor: Aktualizováno

Jak útok funguje

XML umí v deklaraci <!DOCTYPE> definovat entity — zkratky, které parser při čtení nahradí jejich obsahem. Externí entita nemá obsah zapsaný přímo, ale odkazuje na soubor nebo URL (SYSTEM "…"). Když parser externí entity rozbaluje a aplikace pak hodnotu vrátí uživateli, stačí útočníkovi poslat takovýto dokument:

Podvržený požadavek
<?xml version="1.0"?>
<!DOCTYPE objednavka [
  <!ENTITY obsah SYSTEM "file:///etc/passwd">
]>
<objednavka>
  <poznamka>&obsah;</poznamka>
</objednavka>

Parser místo &obsah; vloží obsah souboru /etc/passwd a aplikace, která poznámku zobrazí v potvrzení nebo chybové hlášce, ho pošle zpátky:

Odpověď serveru
<odpoved>
  <stav>Objednávka přijata</stav>
  <poznamka>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
…</poznamka>
</odpoved>

Stejně tak lze číst konfigurační soubory s hesly k databázi, privátní klíče nebo zdrojový kód. Chyba není v XML jako takovém, ale v tom, že parser ve výchozím nastavení důvěřuje dokumentu — a dokument přišel zvenku.

Další podoby: SSRF, slepé XXE a billion laughs

  • SSRF (Server-Side Request Forgery) — entita s adresou http://… přiměje server poslat požadavek za útočníka, typicky do vnitřní sítě nebo na metadata cloudu, kde bývají přístupové klíče.
  • Slepé XXE — aplikace hodnotu nikam nevypíše, ale přes parametrické entity a externí DTD na serveru útočníka se dají data vynést v URL požadavku. Že se nic nezobrazuje, tedy neznamená, že jsi v bezpečí.
  • Billion laughs (entitní bomba) — nejde o soubory, ale o paměť: vnořené entity se při rozbalení násobí, z pár set bajtů jsou gigabajty a server spadne. Jak ji zastavit, popisuje sekce limity rozbalování entit.
SSRF přes entitu
<!DOCTYPE dotaz [
  <!ENTITY meta SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<dotaz>&meta;</dotaz>
Billion laughs (zkráceno)
<?xml version="1.0"?>
<!DOCTYPE lol [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
  <!-- … a tak dál až po lol9: 10⁹ kopií slova „lol“ -->
]>
<lol>&lol2;</lol>

Billion laughs: limity rozbalování entit

Entitní bomba nepotřebuje soubory ani síť, používá jen interní entity definované v DOCTYPE. Vypnutí externích entit proti ní proto nepomůže. Zastaví ji jen zákaz DTD nebo limit, kolik textu smí rozbalení entit vytvořit.

Nestačí ani omezit hloubku vnoření entit. Varianta quadratic blowup má jedinou úroveň: jednu dlouhou entitu, na kterou se odkazuje mnohokrát.

Quadratic blowup (zkráceno)
<?xml version="1.0"?>
<!DOCTYPE q [
  <!ENTITY a "xxxxxxxx…">   <!-- 50 000 znaků -->
]>
<q>&a;&a;&a;&a;&a;…</q>     <!-- 50 000 odkazů = 2,5 miliardy znaků -->

Současné parsery proto měří zesílení, tedy poměr rozbaleného textu ke vstupu, nebo celkový počet a velikost expanzí. Plnou bombu ze předchozí sekce (devět úrovní, 10⁹ kopií) jsme 4. 10. 2026 spustili proti výchozí konfiguraci:

Prostředí (ověřená verze)Výsledek pro billion laughs (lol9, 794 B)Ochrana
PHP 8.4, libxml2 2.15Maximum entity amplification factor exceededpoměr rozbaleného textu ke vstupu; platí i s LIBXML_NOENT a LIBXML_PARSEHUGE
Python 3.14, Expat 2.7.4 (ElementTree, minidom)limit on input amplification factor … breachedpoměr zesílení v Expat; starší Expat ochranu nemá
Java 25, JAXP DOMJAXP00010001: more than "2500" entity expansionsjdk.xml.entityExpansionLimit, totalEntitySizeLimit (100 000)
.NET 10, XmlReader výchozíDTD is prohibitedDtdProcessing.Prohibit
.NET 10, DtdProcessing.Parse / XmlDocumentexceeded a limit set by MaxCharactersFromEntitiesvýchozí limit 10 000 000 znaků

Quadratic blowup z ukázky výše odmítly všechny tři parsery, které DTD zpracovávají (libxml2, Expat i JAXP přes totalEntitySizeLimit).

Ochrana tedy závisí na verzi knihovny, ne na jazyku. Expat ji má od verze 2.4.0 (2021). Python ji ale používá jen tehdy, pokud běží s dostatečně novým Expat, protože systémové sestavení se může linkovat proti starší systémové knihovně. JDK do verze 23 povoloval 64 000 expanzí, od JDK 24 jen 2500. Aplikace, které limity samy zvedají nebo je vypínají (MaxCharactersFromEntities = 0, jdk.xml.entityExpansionLimit=0), jsou zranitelné i na nové verzi.

Výslovné limity tam, kde DTD potřebuješ
// .NET: DTD potřebuješ → povol ji, ale s menším limitem
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Parse,
    XmlResolver = null,
    MaxCharactersFromEntities = 1_000_000,   // 0 = bez limitu, nikdy pro cizí vstup
    MaxCharactersInDocument = 50_000_000,
};

// Java: limity jsou výchozí od JDK 24; ve starších verzích nebo pro zpřísnění je nastav
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setAttribute("jdk.xml.entityExpansionLimit", 2500);
dbf.setAttribute("jdk.xml.totalEntitySizeLimit", 100_000);

# Python: zkontroluj verzi Expat, se kterou interpret běží
import pyexpat
print(pyexpat.EXPAT_VERSION)    # dokumentace Pythonu varuje před verzemi pod 2.7.2

Pořadí obrany zůstává stejné: nejdřív DOCTYPE odmítni (viz nastavení podle jazyka). Limity rozbalování jsou druhá vrstva pro aplikace, které DTD opravdu zpracovávají. Přidej k nim limit velikosti vstupu ještě před parserem, protože ten bombu nezastaví, ale omezí velký dokument bez entit. Zdroje: Python XML security, java.xml: limity zpracování, JDK 24 release notes a limity XmlReaderSettings v .NET.

Obrana: vypni DTD a externí entity

Nejspolehlivější je dokumenty s <!DOCTYPE> vůbec nepřijímat — běžná API (faktury, feedy, SOAP) je nepotřebují. Kde DTD potřebuješ, vypni aspoň načítání externích entit a externích DTD a nastav limity na rozbalování. Konkrétní nastavení se liší podle jazyka:

Prostředí / parserModel a konfiguraceCo hlídat
PHP 8 / libxml2DTD a entity závisí na volbách knihovnyNezapínej LIBXML_NOENT ani LIBXML_DTDLOAD pro cizí vstup; omez resolver.
C# / .NET XmlReaderNastav DtdProcessing.Prohibit a XmlResolver=nullZabezpeč také XmlDocument a zvlášť XSD/XSLT; stanov MaxCharactersInDocument.
Java / JAXPDOM, SAX a StAX mají různé factoryNastav zákaz DTD, externích entit a externích přístupů pro každou fázi.
Python ElementTree / minidomChování entity expansion závisí také na ExpatPro cizí vstup používej defusedxml a vstupní limity.
Python lxmlVýchozí resolve_entities se měnilo mezi verzemiNastav resolve_entities=False, load_dtd=False, no_network=True; hlídej místní zdroje.
JavaScript v prohlížeči / DOMParserNestahuje externí XML entityKontroluj parsererror a velikost; vložení výsledku do HTML je samostatná bezpečnostní fáze.
Node.js / TypeScriptZáleží na knihovně, nikoli na samotném runtimeRozliš @xmldom, saxes a bindingy libxml2; omez DTD, vstup a chování resolveru konkrétní knihovny.
Go / encoding/xmlExterní entity automaticky nenačítáNepřidávej cizí mapu Entity; kontroluj namespace, limity a pokud aplikace DTD nepotřebuje, odmítej DTD Directive.
Rust / quick-xmlReader dává události, není obecný resolver externích zdrojůOdmítej DocType, omez soubor i události; ověř zvlášť deserializaci a NsReader.
C / libxml2Chování závisí na verzi, options a loaderuNepovoluj načítání DTD/rozbalování entit pro cizí data; samotné NONET neblokuje místní soubory.
C++ / pugixmlStromový parser bez automatického externího DTD resolveruOmez vstup a hloubku; bezpečnost jiných C++ XML knihoven z tohoto chování neodvozuj.
Kotlin / JVM JAXPStejná factory a pravidla jako v JavěPoužij zabezpečené JAXP nastavení; Kotlin jazyk nemění přístup XML procesoru k URI.
PowerShell 7 / .NET XMLPoužij XmlReader s výslovnými settingsDtdProcessing.Prohibit, XmlResolver=null a znakový limit; nespouštěj cizí text jako příkaz.

Tabulka pokrývá jazyky webu, ne všechny jejich knihovny. Výchozí hodnoty nejsou jednotná bezpečnostní garance. Použij konfiguraci a verzi z jazykové kapitoly; pro lxml ověř také aktuální XMLParser API. Parser zabezpeč samostatně od validátoru a transformátoru.

PHP

PHP
// PHP 8 s libxml ≥ 2.9: externí entity se ve výchozím stavu NEnačítají.
$doc = new DOMDocument();
$doc->loadXML($xml);                       // bezpečné

// Tohle ochranu vypíná — u cizího XML nikdy:
$doc->loadXML($xml, LIBXML_NOENT);         // nahradí entity jejich obsahem = XXE
$doc->loadXML($xml, LIBXML_DTDLOAD);       // načte externí DTD
simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);

// Nejpřísnější varianta: dokument s DOCTYPE vůbec nepřijmout
if ($doc->doctype !== null) {
    throw new RuntimeException('DOCTYPE není povolený.');
}

C# / .NET

C#
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,   // dokument s DOCTYPE vyhodí výjimku
    XmlResolver = null,                       // nic se nestahuje ani nečte z disku
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);

// Starý XmlDocument (.NET Framework < 4.5.2 měl nebezpečné výchozí nastavení):
var xmlDoc = new XmlDocument { XmlResolver = null };

Java

Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Nejjistější: DOCTYPE úplně zakázat
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Pokud DOCTYPE potřebuješ, vypni aspoň externí entity a DTD
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();

Python

Python
# Doporučeno: knihovna defusedxml se stejným API jako standardní moduly
import defusedxml.ElementTree as ET
strom = ET.fromstring(xml)          # DOCTYPE s entitami vyhodí výjimku

# lxml: entity neřešit a nic nestahovat
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
strom = etree.fromstring(xml, parser)

Node.js

JavaScript
// libxmljs2: noent: true by entity rozbalilo — ponech výchozí false
const doc = libxmljs.parseXml(xml, { noent: false, nonet: true });

// fast-xml-parser ani xml2js externí entity nenačítají,
// DOMParser v prohlížeči také ne.

Jak ověřit, že je aplikace chráněná

  • Pošli na vlastní testovací prostředí dokument s entitou na neškodný soubor (např. file:///etc/hostname) a sleduj, jestli se obsah objeví v odpovědi nebo logu.
  • Hledej v kódu příznaky, které ochranu vypínají: LIBXML_NOENT, DtdProcessing.Parse, resolve_entities=True, noent: true.
  • Nezapomeň na nepřímé cesty: nahrané soubory DOCX, XLSX a SVG jsou také XML, stejně jako SAML a SOAP zprávy.
  • Parsery aktualizuj — mnoho oprav přišlo jen jako změna výchozího nastavení v novější verzi.

Poznámka: Útoky zkoušej jen na systémech, které vlastníš nebo máš povolení testovat. Nástroje na XML.cz běží v prohlížeči, jehož parser externí entity nenačítá, takže vložení podezřelého dokumentu do validátoru nic nespustí.

XInclude a další fáze zpracování

Vypnutí externích entit nepokryje pozdější zpracování XInclude, načtení schématu, XSLT document() ani aplikační stahování odkazů. XInclude je samostatná operace, která může vložit XML nebo text z URI. Na nedůvěryhodných datech ji nezapínej nebo použij striktní resolver.

Zakaž nechtěné souborové i síťové zdroje; volba „nonet“ nemusí zakazovat místní soubory. Vedle konfigurace parseru omez velikost vstupu, hloubku, počet uzlů, délku textů, čas a paměť. Vypnutí DTD samo nebrání obřímu dokumentu bez entit.

Testuj zvlášť parser, validaci a transformaci. Bezpečné načtení v první fázi nezaručuje bezpečnost dalších knihoven. Pro XML Signature navíc ověř, že aplikace používá právě podepsané uzly.