XML External Entities (XXE): Reading the Server's Files
SQL injection smuggles code into a query. XSS smuggles code into a page. XXE smuggles code into an XML parser — and makes the server read its own files or reach its own internal network on your behalf.
The XML machinery
An XML document can start with a DOCTYPE, which can contain a DTD (Document Type Definition) that declares shortcuts called entities:
<!DOCTYPE note [
<!ENTITY greeting "hello">
]>
<note>&greeting;</note>
An entity is just a named value, and &greeting; is its reference. When a parser processes the document, it expands &greeting; into hello. That alone is harmless. The problem starts when an entity can point at something outside the document:
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
SYSTEM tells the parser: go fetch the contents of this URL or file. If the parser honors that, &xxe; expands to the entire contents of /etc/passwd — and if the app then echoes the document back to you (a preview, a rendered page, a converted file), you just read the server's files.
Where XXE hides
Anywhere an application parses XML you can influence:
- An XML viewer or "prettifier" that echoes your document back.
- File uploads that accept SVG (SVG is XML) — a profile picture is the classic case.
- Document converters that process .docx (a ZIP of XML), .xls, .xml, or PDF-to-X formats.
- Config imports, SOAP/XML-RPC APIs, or anything that takes an XML payload.
You're not limited to file://. Point an entity at http://10.0.0.12/export and the server makes that request from its own network position — SSRF. You can read internal services that your own machine can't reach, and the app shows you the response.
Blind XXE: when the response goes quiet
Sometimes the app stops echoing the expansion (a filter, a format that doesn't render it). The file contents never appear in the response. You can still exfiltrate them out-of-band: declare a parameter entity (written %name;) in the DTD that fetches a remote DTD you host, and that remote DTD reads the local file and requests your listener with the contents in the URL. The app never shows you the data — your server logs do.
The fix
The parser should never process DTDs or external entities from untrusted input. Libraries like libxml2 (XML_PARSE_NOENT | XML_PARSE_DTDLOAD off), Python's lxml with resolve_entities=False, Java's DocumentBuilderFactory with setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) — every mainstream parser has an explicit flag, and the default is often too permissive. And the broader lesson: don't parse untrusted XML at all if you can avoid it (prefer JSON, or use a safe parser with DTDs off).
Try it (terminal)
The lab beside this lesson is a document viewer that parses XML you submit:
curl "http://10.10.10.20/xml-viewer" -d @payload.xmlwith a normal document — note it echoes the parsed XML.- Send a DOCTYPE with an external entity pointing at
file:///etc/passwdand the response contains the file — that's XXE.
Why this matters
XXE is a favorite of real engagements because it's usually a single request to go from "a web app" to "files on the server" or "reach into the internal network". It's been at or near the top of the OWASP Top 10 for years. The skill you're building is recognizing "this app parses XML I control" — the moment you see an XML viewer, an SVG upload, or a document converter, XXE is on the checklist.