Diagnosing XXE Exposure: When Your XML Parser Makes Network Calls You Never Asked For
Unexpected outbound requests, file contents in errors, or a tiny XML file pinning a CPU all point to external entity resolution. Here's how to diagnose XXE exposure and harden Java, .NET, and Python parsers.
26 Jul 2026, 23:09 UTC

The symptom usually arrives sideways: a log entry showing an outbound HTTP request from a service that should only be parsing documents, a stack trace containing the contents of a local file, or a small uploaded XML file that pins a CPU core. All three point to the same root cause — an XML parser resolving external entities it was never explicitly told to ignore. This guide walks through recognizing the condition, confirming it safely, and hardening the parser in Java, .NET, and Python.
Recognizing the condition
XML External Entity (XXE) processing happens when a parser honors a DOCTYPE declaration that defines entities referencing external resources — files on disk, URLs, or other network endpoints. Most parsers historically did this by default, because the XML specification describes it and early XML workflows relied on it.
The recognizable conditions:
- Local file contents (for example
/etc/passwdor config files) appear in parse errors, responses, or logs after XML input is processed. - The application host makes unexpected DNS lookups or HTTP connections when attacker-influenced XML arrives — a server-side request forgery (SSRF) pattern.
- A tiny document causes exponential memory or CPU consumption — the "billion laughs" attack, where nested entity definitions expand recursively.
- Parse errors mention
DOCTYPEat all, which tells you DTDs are being processed when they probably shouldn't be.
Symptom-to-cause table
| Observation | Likely cause |
|---|---|
| File contents disclosed in output or errors | External general entities enabled |
| Outbound connections to internal or external hosts | External entities or external DTD loading enabled |
| Huge CPU/memory from a small document | Entity expansion not limited |
| Errors referencing DOCTYPE on any input | DTD processing enabled at all |
Ordered diagnostic checks
- Confirm the input reaches an XML parser. Trace the endpoint or message handler to the actual parse call. Frameworks sometimes parse XML inside deserialization layers you didn't write.
- Inspect the parser factory configuration. Look for
DocumentBuilderFactory,SAXParserFactory,XmlReaderSettings, orxml.etree/lxmlusage and check what features are set. Absence of hardening settings is the finding — defaults are often unsafe. - Send a canary document. In a controlled test environment only, parse a document whose external entity points at a listener you operate (a request-bin style endpoint). If your listener receives a hit, resolution is happening. This resembles a real attack payload, so get authorization and never run it against production or third-party systems.
- Check for custom resolvers. A custom
EntityResolver(Java) orXmlResolver(.NET) can re-enable fetching even when features look hardened. Search dependencies too — SOAP stacks, SAML libraries, and framework deserializers create their own parser instances that your application-level hardening never touches.
Fixes tied to findings
Java
Wherever you build a parser factory, disable DTDs outright, or at minimum block external entities. Run this in application code; no special permissions needed:
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(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);Exact feature URIs and defaults vary by JDK vendor and version, so verify against the runtime you actually deploy. If disallow-doctype-decl breaks legitimate documents (some older SOAP and XSLT integrations use internal DTDs), fall back to the external-entity flags alone.
.NET
Use XmlReader with explicit settings rather than the legacy XmlTextReader:
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);XmlResolver = null matters: without it, a resolver capable of fetching URLs may still be in play if DTD processing is ever relaxed.
Python
The standard-library parsers based on expat don't fetch external entities, but they are still exposed to entity-expansion attacks, and lxml can resolve external entities depending on configuration. The pragmatic fix is the defusedxml package, which wraps the standard parsers with safe defaults:
import defusedxml.ElementTree as ET
tree = ET.parse("input.xml")Install it with pip install defusedxml and replace the standard imports.
Defense in depth
Regardless of language: reject documents containing a DOCTYPE at the validation layer before parsing, centralize parser creation in a shared factory helper so every call site gets the hardened settings, and egress-filter the hosts running parsers so a missed configuration cannot exfiltrate data. If the data format is negotiable, prefer JSON for untrusted input.
Verifying the fix
- Re-run the canary document against the hardened parser and confirm your listener receives nothing and the entity is not expanded.
- Parse a small entity-expansion document and confirm it is rejected quickly instead of consuming memory.
- Grep the codebase and dependency list for parser instantiations and confirm each path uses the shared hardened factory.
- Add a regression test asserting that DOCTYPE-bearing input is rejected or parsed without resolution, so a future library upgrade cannot silently re-enable it.
When to escalate
Escalate to security incident response if you find evidence of actual exfiltration (canary hits from production, file contents in logs), if the vulnerable parser is reachable from an unauthenticated endpoint, or if a legacy component cannot be hardened and must be isolated or replaced. A configuration gap you found and fixed quietly is a hardening task; a gap with evidence of exploitation is an incident.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.