7 min read

Intermediate

XXE Remediation

Risk Severity
Critical
Fix Effort
Low (Quick Fix)
Est. Time
30-60 minutes
Reference
A02:2025 CWE-611

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 can lead to reading sensitive files like /etc/passwd or web.config, internal network scanning, denial of service via billion laughs attack, and in some cases remote code execution.

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

The fix for XXE is remarkably simple: disable DTD processing entirely or disable external entity resolution. Most applications don't need DTDs, so disabling is safe.

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 protections

PHP

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

XXE Injection PortSwigger medium
Basic XXEBlind XXEXXE via file upload

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/shadow where 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-decl plus both external-entity features and setXIncludeAware(false). Python: defusedxml or a parser with entity resolution off. .NET: DtdProcessing.Prohibit and 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