XXE Remediation
XML External Entity (XXE) attacks exploit vulnerable XML parsers to read local files, perform SSRF, or cause denial of service. The fix is straightforward: disable DTD and external entity processing.
XXE Impact
XXE Attack & Defense
Understanding XXE
Vulnerable XML
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>Entity &xxe; is replaced with file contents
Attack Variants
- • Classic XXE: Read local files
- • Blind XXE: Out-of-band data exfiltration
- • Error-based XXE: Data in error messages
- • SSRF via XXE: Internal network access
- • Billion Laughs: DoS via entity expansion
Disable External Entities
Simple Fix
Java
// DocumentBuilderFactory
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.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
// SAXParserFactory
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
// XMLInputFactory (StAX)
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);Python
# Using defusedxml (recommended - drop-in replacement)
import defusedxml.ElementTree as ET
tree = ET.parse('file.xml')
# Standard library with manual fixes
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True)
tree = etree.parse('file.xml', parser)
# For xml.etree (limited protection)
import xml.etree.ElementTree as ET
# Note: xml.etree doesn't support external entities by default,
# but defusedxml provides additional protectionsPHP
// Disable external entity loading globally
libxml_disable_entity_loader(true);
// For specific parsing
$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
// Using XMLReader
$reader = new XMLReader();
$reader->setParserProperty(XMLReader::SUBST_ENTITIES, false);.NET / C#
// XmlReader (safe by default in .NET 4.5.2+)
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
XmlReader reader = XmlReader.Create(stream, settings);
// XmlDocument
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null;
doc.LoadXml(xml);
// XDocument (safe by default)
XDocument xdoc = XDocument.Parse(xml);Node.js
// Using libxmljs2 with safe options
const libxmljs = require('libxmljs2');
const doc = libxmljs.parseXml(xml, {
noent: false, // Don't expand entities
nonet: true, // Don't fetch network resources
noblanks: true,
nocdata: true
});
// Or use a safe parser like fast-xml-parser
const { XMLParser } = require('fast-xml-parser');
const parser = new XMLParser();
const result = parser.parse(xml);Testing Verification
XXE Test Payloads
<!-- File read test -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
<!-- SSRF test -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://internal-server/admin">
]>
<!-- Billion laughs DoS test (use with caution) -->
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<!-- Expected result: All should fail or return errors -->Common Mistakes
Only Blocking file://
Attackers can use http://, ftp://, gopher://, or custom protocols
Correct Approach
Disable all external entity processing at the parser level
Trusting Content-Type
XML can be sent with any content type (application/json, image/svg+xml)
Correct Approach
Apply XXE protection to all XML parsing, regardless of content type
XXE Prevention Checklist
- ☐ DTD processing disabled in all XML parsers
- ☐ External entity resolution disabled
- ☐ XInclude disabled
- ☐ Using safe libraries (defusedxml, etc.)
- ☐ All XML endpoints protected (SOAP, SVG, DOCX, etc.)
- ☐ Content type not trusted for determining parser
XXE Labs
Practice exploiting and preventing XXE vulnerabilities.
Evidence to Capture
The parsing request: The full XML body with the doctype intact, the Content-Type sent, and the endpoint that parsed it. Include any JSON endpoint that turned out to accept XML as well.
File read proof: the response fragment containing the first lines of the target file (for example root:x:0:0), not a generic 500 or a parser error.
Out-of-band interaction: The DNS or HTTP hit from the parameter entity with its source address, which is the only proof for blind XXE where no content is reflected.
Parser and version: The library and version that handled the document, from the stack trace, Server header or dependency file, plus whether DTD processing was enabled by default in that version.
Secondary entry point: The upload path that reached an XML parser — DOCX, XLSX, SVG, SAML metadata — when direct XML posting was blocked but the file path was not.
Likely False Positives
- Doctype rejected by the parser: A doctype-is-disallowed or external-entities-not-supported error means the control is present. Capture it as evidence of the fix, not of the bug.
- XML stored, never parsed: If the document is saved or echoed without a parser, no entity is resolved. Include an entity reference in the body and see whether it expands.
- Blocked only by egress: A failed file read with a successful DNS callback still means the parser resolves entities. Rate it on that evidence rather than calling it unexploitable.
- SVG that is rasterised: An image pipeline that decodes and re-encodes the SVG never hands the XML to a vulnerable parser. Confirm the parser exists before reporting.
- SSRF mislabelled as XXE: A URL-fetch parameter in a JSON API involves no DTD. Classify by the mechanism that produced the outbound request.
Testing Checklist
Discovery
- ☐ Find every XML ingestion path: SOAP, SAML and SSO metadata, RSS and feed import, SVG, office documents, and any endpoint that accepts application/xml.
- ☐ Check whether endpoints documented as JSON-only still parse XML when the Content-Type is changed.
- ☐ Identify the parser library per service, since the configuration differs per language and framework.
Confirmation
- ☐ Send a benign doctype and internal entity first, to establish that DTD processing is on at all.
- ☐ Then request a local file by absolute path, and a parameter entity pointing at your listener for the blind case.
- ☐ Repeat through the file-upload path and with alternative content types, then re-test after the fix and capture the rejection message.
Impact
- ☐ Read a specific sensitive file — application config with database credentials, a private key, or
/etc/shadowwhere the process permits — rather than any file at all. - ☐ Confirm whether the same primitive reaches the metadata service or internal HTTP endpoints, which raises it to credential theft.
- ☐ Check whether entity expansion is bounded, because an unbounded expansion path is a denial of service even without data exfiltration.
Remediation Summary
Structural fix
- • Disable DTDs and external entities at parser construction in every service, and make that the default in the shared wrapper rather than an opt-in per call site.
- • Java:
disallow-doctype-declplus both external-entity features andsetXIncludeAware(false). Python:defusedxmlor a parser with entity resolution off. .NET:DtdProcessing.Prohibitand a null resolver. - • Replace XML with JSON where the contract allows it, since the class cannot arise in a format with no entities.
Compensating control
- • Run the parser in a worker with restricted filesystem and network access, so a missed configuration is contained.
- • Reject documents carrying a doctype at the edge, and re-encode or reject office and SVG uploads before they reach a parser.
- • Keep a regression test that posts a file-read payload to every XML endpoint and asserts a rejection.
CWE References: CWE-611