HolyGhost logoHolyGhost
← cd ..
Analysis

XXE: When an XML Parser Reads Files It Should Not

XML has a feature that lets a document pull in outside content. Point it at a local file or an internal server and a helpful parser becomes a data leak. Here is how XML External Entity attacks work.

HolyGhost··9 min read

A developer builds a feature that accepts a document upload. Invoices, perhaps, or configuration files, or those tidy office documents that everyone has on their desktop. The application opens each file, reads out the useful bits, and files them neatly away. It has parsed thousands of these without complaint. What the developer does not realise is that many of those files are secretly XML underneath, and that the library reading them will, if asked politely in the right way, wander off and fetch the server's own password file and paste it into the results. The feature was never designed to read secret files. It just never learned to say no. That is XXE.

XXE, short for XML External Entity, is a lovely example of a legitimate feature becoming a liability. XML stands for Extensible Markup Language, a common text format for structured data that wraps values in tags like <name>Alice</name>. It has been around for decades and turns up everywhere. One of its features lets a document pull in external content. When an application parses attacker supplied XML with that feature switched on, the attacker can make the server read local files or reach internal systems. A parser, by the way, is simply the piece of software that reads a document and turns it into data the program can use. Here is how it works and how to shut it off.

Scope

This is a defensive explainer of a well known vulnerability class, for use on systems you own or are authorised to test.

The feature: entities

XML lets you define entities, which are essentially named placeholders that get substituted into the document wherever you refer to them. You may already have met the built in ones without realising it. When a web page needs to show a literal < character, it writes &lt;, and the parser swaps that in for the real symbol. That is an entity. You can also define your own:

Define an entity, then use it:
   <!DOCTYPE note [ <!ENTITY company "HolyGhost Ltd"> ]>
   <note>Written by &company;</note>
 
The parser replaces &company; with "HolyGhost Ltd".

That part is harmless. The danger lives in a special kind called an external entity. The <!DOCTYPE ...> line above is a Document Type Definition, or DTD, a section that can declare entities and rules for the document. An external entity, marked with the keyword SYSTEM, does not hold a fixed piece of text. Instead it tells the parser to go and fetch content from somewhere, a file path or a web address, and insert whatever it finds.

That is the whole hinge. If a parser honours external entities and you can supply the XML, you can define an entity that points wherever you like, including places you were never meant to see:

An external entity that references a local file:
   <!DOCTYPE x [ <!ENTITY leak SYSTEM "file:///etc/passwd"> ]>
   <data>&leak;</data>
 
A vulnerable parser fetches the file and places its contents where &leak; sits.

Here file:///etc/passwd is a local file address pointing at a Linux file that lists user accounts, a favourite first target because reading it proves the attack works. If the application then echoes that data back in its response, for example by returning the parsed value on screen, the attacker reads the file's contents straight out of the reply. The parser did nothing wrong by its own rules. It was told to fetch a file, so it fetched a file.

When the answer never comes back

You might reasonably ask what happens if the application does not echo the parsed data. Surely then the attacker gets nothing? Not quite. This is called blind XXE, where the file is read but never shown, and attackers have a clever way around it. They host their own malicious DTD on a server they control, and craft the XML so the fetched file contents get bundled into an outbound request back to that server. The secret data leaves through the back door instead of the front. This is worth knowing because "we do not display the XML we parse" is not, on its own, a defence.

What it enables

XXE is mostly about reaching things the server can access but you should not. The parser has the server's own permissions and network position, and XXE quietly borrows them:

  • Local file disclosure. Read configuration files, secrets, source code, and credential stores, much like directory traversal, but through the XML parser rather than a file path in a request.
  • Server side request forgery. Point an entity at an internal web address and the server fetches it for you. Server side request forgery, or SSRF, means tricking the server into making requests on the attacker's behalf, often to internal systems the attacker cannot reach directly. This is the doorway explored in SSRF and the cloud metadata endpoint, where that same trick pries loose cloud credentials.
  • Denial of service. A famous variant, the "billion laughs" attack, defines entities that expand into each other in layers, each one referring to the one below several times over, until a tiny document balloons into gigabytes and exhausts the server's memory. Denial of service simply means knocking a system offline rather than stealing from it.
The "billion laughs" idea, simplified:
   entity a = "lol"
   entity b = &a; &a; &a; ...        (ten copies of a)
   entity c = &b; &b; &b; ...        (ten copies of b)
   ...and so on. A few short lines expand into billions of characters.

Anywhere an application accepts XML, and many do behind the scenes, in document formats, SOAP web services, application programming interface calls, and file uploads, XXE is worth checking.

It hides in formats you forget are XML

Plenty of file types are XML underneath, including many office document formats and SVG images. An upload feature that parses one of these can be quietly vulnerable to XXE even when no one thinks of it as an XML endpoint.

Why it happens so often

If the fix is easy, and it is, why is XXE still so common? The answer is history. For a long time, many popular XML libraries shipped with external entity processing switched on by default. A developer who reached for the standard parser and used it in the obvious way inherited the vulnerability without writing a single risky line. The unsafe behaviour was the out of the box behaviour. Newer library versions have gradually flipped this, defaulting to safer settings, but plenty of older code and older libraries are still in service, still trusting whatever XML arrives. This is why simply keeping libraries current does real security work, and why you should never assume an inherited codebase is safe just because nobody touched the parser.

Shutting it off

The good news is that XXE has a clean, decisive fix. Unlike injection flaws where you fight an endless battle against clever input, here you can turn off the dangerous feature completely and be done.

  1. Disable external entities and document type definitions in the parser. Almost no application actually needs external entity resolution, and very few need DTDs at all. Turning off DTD processing, or at the very least external entity resolution, closes the hole outright. There is nothing to bypass because the parser will simply refuse to fetch anything. This is the fix, not a mitigation, and every major XML library documents the exact setting to use. Set it once, correctly, everywhere you parse XML.

  2. Prefer simpler formats. If you have a choice about how data arrives, JSON does not have this class of feature at all. JSON, a lightweight data format built from lists and key and value pairs, has no notion of entities pulling in outside content, so it has no equivalent risk. Using it where you can removes the danger entirely rather than merely defusing it.

  3. Keep parsers updated and least privileged. Current library versions often default to safer behaviour, so staying up to date quietly closes holes you might not know you had. And least privilege, running the parser in an account with as little access as possible, means that even a successful XXE finds little worth reading and cannot reach far.

Fix it in one shared place

Applications often parse XML in several spots. Rather than hardening each one separately and hoping you found them all, wrap your XML parsing in a single shared, safely configured helper and have every part of the code use it. One correct setting, applied everywhere, beats scattered fixes that are easy to forget on the next new feature.

Authorised testing only

XXE reads files and reaches internal systems. Only probe for it on applications you own or have explicit written permission to test. Reading a server's files without authorisation is unlawful, regardless of how easy the flaw makes it.

The takeaway

XXE abuses XML's external entity feature to make a parser fetch local files or internal web addresses on the attacker's behalf, leading to file disclosure, server side request forgery, or denial of service, and it works even when the parsed data is never shown back to you. It stays common largely because older parsers enabled the feature by default. The fix, though, is refreshingly clear: disable external entities and DTD processing in your XML parser, prefer plainer formats like JSON where you can, keep libraries current, and remember that many innocent looking file types are XML underneath. Turn off the feature almost nobody needs, apply that setting everywhere you parse XML, and XXE simply goes away.