XML external entity (XXE) injection: how it works, how testers prove it, how to fix it
XML external entity (XXE) injection is a flaw where an XML parser processes a document type definition that points to files or URLs outside the document, so the server reads or fetches things the attacker chose. Fix it by disabling DTDs and external entities in the parser. Testers prove it with a harmless file or a callback, never by reading secrets.
XML external entity (XXE) injection is a flaw where an XML parser processes entity references that point outside the document, so the server reads local files or makes network requests that an attacker chose. MITRE describes CWE-611 as a product that processes an XML document containing entities with URIs that resolve to documents outside the area the product intends to control. XXE usually comes from parser defaults or settings, not from application logic.
Testing for XXE is part of web application penetration testing and API penetration testing. Testing needs written authorization from the system owner.
What is XXE and why does an XML parser read files?#
An XML parser reads files because XML allows a document type definition (DTD) that can declare entities, and an entity can name an external resource by URI. When the parser expands such an entity, it fetches the resource and puts the content into the document.
That feature was designed for document assembly. It becomes a flaw when an application parses XML from an untrusted source with these features on. The three outcomes PortSwigger lists are reading files from the server, forcing the server to send requests (SSRF), and blind variants where the data is carried out of band. A fourth, resource exhaustion, comes from entity expansion.
OWASP merged XXE into Security Misconfiguration in the 2021 Top 10. In the Top 10:2025, security misconfiguration is A02.
How does an XXE attack work?#
An XXE attack works by sending an XML body that declares an external entity and then references it where the application echoes or uses the value. The parser expands the entity before the application sees the data.
Vulnerable invoice import (fictional app)
This is a demonstration. LedgerLeaf is a fictional billing app on app.example.com, and no real customer system was tested.
The app accepts an XML invoice at POST /api/invoices/import and returns the customer name.
// Vulnerable: default parser settings, untrusted body
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = db.parse(request.getInputStream());
String customer = doc.getElementsByTagName("customer").item(0).getTextContent();
return Map.of("customer", customer);
Normal body:
<invoice><customer>Acme Ltd</customer></invoice>
An authorized tester sends a DOCTYPE that declares an entity for a harmless test file the tester placed on the server, for example a file in the app's own test folder:
<!DOCTYPE invoice [ <!ENTITY probe SYSTEM "file:///srv/app/test-marker.txt"> ]>
<invoice><customer>&probe;</customer></invoice>
If the response returns the content of test-marker.txt as the customer name, the parser resolved the external entity. The saved request and response, with a timestamp, are the evidence. Control: the same request without the entity returns Acme Ltd, and a request that names a nonexistent file returns an error.
This is the pattern OWASP and PortSwigger document. For a case where the response never shows the value, see the blind section below.
What are the main XXE variants?#
XXE has a handful of variants, and they differ in what the attacker sees and what the server does. The table maps each to its evidence and to the usual fix.
| Variant | What happens | Evidence a tester collects | Primary fix |
|---|---|---|---|
| File retrieval | An entity points at a local file and its content appears in the response | A harmless test file read back in the response | Disable DTDs and external entities |
| SSRF through XXE | An entity points at an internal or external URL, so the parser makes the request | A callback logged on a host the tester controls | Disable external entities, restrict egress |
| Blind XXE (out of band) | No content in the response, but the parser still fetches the resource | A DNS or HTTP hit with a timestamp from the target network | Disable external entities and external DTD loading |
| Error-based | The file content or path appears in a parse error | An error message that names the test file | Disable DTDs, show generic errors |
| XInclude | The attacker controls a value, not the whole document, and injects an include element | The included test file appears in the output | Turn off XInclude, validate input |
| Entity expansion (DoS) | Nested entities expand into very large output | Parser memory or time grows on a bounded, agreed test | Disable DTDs, set parser limits |
XXE and SSRF overlap. XXE is a parser flaw that can cause SSRF. SSRF is a request-making flaw that can come from many features, including XML parsing.
Where is the hidden XXE attack surface?#
XXE hides wherever an XML parser sits behind a feature that does not look like XML. PortSwigger lists three common places: file uploads that parse XML formats, endpoints that accept XML if the content type is changed, and parameters that are inserted into a server-side XML document.
| Surface | Why it parses XML | What to check |
|---|---|---|
| SVG image upload | SVG is XML | Does the server render, resize or sanitize the SVG? |
| DOCX, XLSX, PPTX upload | Office files are ZIP archives of XML | Does the app extract text or data from the file? |
| SAML login | SAML messages are XML | Does the service provider parse the assertion with a hardened parser? |
| SOAP and legacy APIs | The message is XML | Do the endpoints allow a DOCTYPE? |
| RSS and Atom import | Feeds are XML | Is the feed fetched and parsed server-side? |
| JSON endpoint | Some frameworks accept both | Does changing Content-Type to application/xml change parsing? |
| PDF and report generators | Some use XML or XSLT internally | Does the generator load remote resources? |
The Apache POI case below shows the Office-file route. File upload flaws and XXE often appear in the same test.
Which real incidents involved XXE?#
XXE has produced many CVEs in widely used libraries. Three records, each checked on NVD on 9 November 2026:
| CVE | Product | Published (NVD) | What the record says |
|---|---|---|---|
| CVE-2012-3489 | PostgreSQL 8.3 to 9.1 (libxml2 support) | 3 October 2012 | The xml_parse function allowed authenticated users to determine whether arbitrary files or URLs exist, and possibly to read content that triggers a parsing error, through an XML value that refers to a DTD or an entity. CWE-611 |
| CVE-2018-1259 | Spring Data Commons 1.13 before 1.13.12 and 2.0 before 2.0.7, with XMLBeam 1.4.14 or earlier | 11 May 2018 | XMLBeam did not restrict external reference expansion, so an unauthenticated remote user could send crafted request parameters against projection-based request payload binding. CWE-611 |
| CVE-2019-12415 | Apache POI up to 4.1.0 | 23 October 2019 | When the XSSFExportToXml tool converted user-provided Excel documents, a crafted document could allow reading files from the local filesystem or internal network resources. CWE-611 |
The three show different routes in: a database function, a web framework binding and an Office-file converter. None needed the application developer to write XML code.
How do penetration testers detect and prove XXE?#
Testers prove XXE with a harmless file or a callback, a control, and a saved response. Excession Labs holds to one evidence rule: a finding needs evidence of a real effect, and a bare "success" response is not proof.
- Find every XML entry point: direct XML bodies, uploads of SVG and Office files, SAML, SOAP, feeds, and endpoints that accept a changed
Content-Type. - Send a baseline request, then a control with a harmless DOCTYPE and no entity reference. Note whether the server rejects DOCTYPE, which is a good sign.
- Declare an entity for a harmless file that the team placed for the test, or for a URL on a host the tester owns. Reference it in a field the response echoes.
- For blind cases, use an out-of-band lookup to the tester's host and record the timestamp and source address.
- Stop at the harmless proof. Do not read configuration files, keys or user data. Do not request cloud metadata unless the written scope approves it.
- Keep the request, the response and the listener log. Report the controls that held, such as an endpoint that rejects DOCTYPE.
When a control shows the first result was misleading, we downgrade or drop the finding. Entity-expansion tests can harm availability, so run them only if the scope approves a bounded test.
How do you prevent XXE?#
Prevent XXE by disabling DTD processing in every parser that handles untrusted XML. If your application needs DTDs, disable external entities and external DTD loading instead. OWASP's cheat sheet gives per-parser settings and prefers rejecting DOCTYPE.
| Parser | Setting | Source note |
|---|---|---|
| Java DocumentBuilderFactory (DOM) | setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) and FEATURE_SECURE_PROCESSING true |
OWASP. setExpandEntityReferences(false) does not block external resources |
| Java SAXParserFactory | Same two settings on the factory before creating the reader | OWASP. Call reader.parse(...) directly |
| Java XMLInputFactory (StAX) | SUPPORT_DTD false and IS_SUPPORTING_EXTERNAL_ENTITIES false |
OWASP. StAX has DTD processing on by default, and does not support FEATURE_SECURE_PROCESSING |
| Java TransformerFactory (XSLT) | FEATURE_SECURE_PROCESSING true, ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_STYLESHEET set to empty strings |
OWASP says this is not a sandbox for untrusted style sheets |
| Python lxml | resolve_entities=False on the parser |
lxml FAQ: lxml 5.x disables external entity expansion by default |
| Python defusedxml | Use its parsers, with forbid_dtd=True where available |
OWASP |
| PHP (libxml) | Do not set LIBXML_NOENT, LIBXML_DTDLOAD or LIBXML_DTDVALID |
PHP manual: libxml 2.9.0 disabled entity substitution by default |
| .NET XmlReaderSettings | DtdProcessing = DtdProcessing.Prohibit |
OWASP: Prohibit throws when a DOCTYPE is present |
| Node.js libxmljs | Do not pass the noent option |
Not covered by the OWASP page. Check the library's documentation for your version |
Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
DocumentBuilder db = dbf.newDocumentBuilder();
The OWASP cheat sheet shows newDefaultInstance() for Java 9 and later. Set the features before you create the builder.
Python
import defusedxml.ElementTree as ET
root = ET.fromstring(xml_text, forbid_dtd=True)
# or with lxml
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
root = etree.fromstring(xml_bytes, parser)
The lxml FAQ says lxml 5.x disables external entity expansion by default. Setting the options explicitly keeps the behavior if you upgrade or downgrade.
PHP
// PHP 8.0 and later: libxml 2.9+ does not load external entities by default.
// Do not pass LIBXML_NOENT, LIBXML_DTDLOAD or LIBXML_DTDVALID.
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET);
// Extra layer (OWASP): the callback must return null
libxml_set_external_entity_loader(function () { return null; });
The PHP manual says libxml_disable_entity_loader has been deprecated since PHP 8.0.0 because external entity loading is disabled by default. On PHP before 8.0 with an old libxml, call it with true. OWASP warns that passing null directly to libxml_set_external_entity_loader() does not block loading. Use the callback form.
C# and .NET
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);
OWASP says XmlReader, XElement and XDocument are safe by default in any .NET version, and that XmlDocument and XmlTextReader are safe from .NET 4.5.2. A reader becomes unsafe only if DtdProcessing is Parse and a non-null resolver is set. In ASP.NET, the httpRuntime target framework must be 4.5.2 or later.
Node.js
// libxmljs: do not enable entity substitution for untrusted input
const doc = libxmljs.parseXml(xml, { noent: false, nonet: true });
Check the documentation of your XML library for its entity and network options. Prefer JSON for new endpoints. If you accept uploads that contain XML, parse them in a separate low-privilege process.
Beyond parser settings, add layers: update XML libraries, reject unexpected Content-Type values, validate and size-limit input, block outbound network access from the parsing service, and show generic error messages.
What is the XXE testing checklist?#
Use this list for each XML entry point, and only inside the agreed scope.
- List every endpoint, upload and import that may reach an XML parser, including SVG, DOCX, XLSX, SAML, SOAP and feeds.
- Test a changed
Content-Typeon JSON endpoints. - Send a baseline, a harmless DOCTYPE with no entity, and a nonexistent-file control.
- Declare an entity for a harmless test file or a callback host, and reference it in an echoed field.
- Test blind: a callback host with a timestamp and source address.
- Test XInclude where only a value is controllable.
- Check error messages for paths and parser names.
- Stop at the harmless proof. Do not read secrets, and do not run expansion tests without written approval.
- Save requests, responses and listener logs. Report the controls that held.
- Review the code for parser creation calls that lack the settings in the table above.
When should you get a professional test?#
Get a professional test when your application accepts XML in any form: SOAP, SAML single sign-on, SVG or Office uploads, feed imports. XXE hides in libraries and in file handling that code review often misses. Blind and file-based cases need a callback listener and careful scope rules.
Excession Labs runs authorized web application penetration tests and API penetration tests. We agree the scope with you in writing, check the evidence for each finding and write the report. You can also apply for the free pentest pilot.
Questions
What is the difference between XXE and SSRF?
XXE is a flaw in how an XML parser handles entities. SSRF is a flaw in how an application fetches URLs. XXE can cause SSRF, because an external entity can point at an internal URL, so the parser makes the request. The fix differs: disable external entities in the parser for XXE, validate destinations and restrict egress for SSRF.
How do you prevent XXE?
Disable DTDs, or at least external entities and external DTD loading, in every XML parser that handles untrusted input. OWASP lists the exact settings for each Java parser, and says to prefer rejecting DOCTYPE declarations. Keep parser libraries up to date, and avoid XML where JSON is enough.
Is my parser safe by default?
It depends on the parser and version. The OWASP cheat sheet says libxml2 from 2.9, lxml 5.x, and many .NET readers do not resolve external entities by default. It also says StAX has DTD processing on by default, and ColdFusion needs a setting on each call. Check the version you run, and set the options anyway.
Can XXE happen without an XML endpoint?
Yes. XML can hide inside SVG images, DOCX and XLSX files, SAML messages, SOAP requests and RSS feeds. A file upload that parses an Office document or an image can reach an XML parser. A JSON endpoint may also accept XML if you change the Content-Type header, so test for that.
Is XXE still in the OWASP Top 10?
In the OWASP Top 10:2021, XXE sits under A05 Security Misconfiguration. In the Top 10:2025, injection is A05 and security misconfiguration is A02. XXE's weakness class is CWE-611.
Sources
- OWASP XML External Entity Prevention Cheat Sheet (checked 9 November 2026)
- OWASP Community: XML External Entity (XXE) Processing (checked 9 November 2026)
- PortSwigger Web Security Academy: XML external entity (XXE) injection (checked 9 November 2026)
- CWE-611: Improper Restriction of XML External Entity Reference (checked 9 November 2026)
- OWASP Top 10:2021 A05 Security Misconfiguration (checked 9 November 2026)
- OWASP Top 10:2025 (checked 9 November 2026)
- NVD: CVE-2012-3489 (PostgreSQL) (checked 9 November 2026)
- NVD: CVE-2018-1259 (Spring Data Commons with XMLBeam) (checked 9 November 2026)
- NVD: CVE-2019-12415 (Apache POI) (checked 9 November 2026)
- lxml FAQ: how do I use lxml safely as a web service endpoint (checked 9 November 2026)
- PHP manual: libxml_disable_entity_loader (checked 9 November 2026)