XML External Entity injection is tracked as CWE-611, and for years it had its own line item in the OWASP Top 10 as A4:2017. OWASP folded it into Security Misconfiguration starting with the 2021 revision (A05:2021), and it has stayed there in the current OWASP Top 10:2025, where Security Misconfiguration moved up to A02:2025 — and that reclassification is the most useful fact in this entire post: XXE is not a code-injection bug like SQL injection, where a developer forgot to parameterize a query. It's what happens when a parser's out-of-the-box defaults are trusted without being checked. An XML parser that resolves a <!DOCTYPE> declaration and follows a SYSTEM identifier will happily read /etc/passwd, reach internal metadata endpoints over HTTP, or exhaust memory through recursive entity expansion — the "Billion Laughs" attack — all from a single crafted document. The fix is not exotic: OWASP's XXE Prevention Cheat Sheet has stated the definitive remediation for years — disable DTD processing entirely wherever the application doesn't need it. What makes XXE persistent in 2026 is that "disable DTD processing" means something different in Java, Python, PHP, and .NET, and teams that patch one stack often assume the others are equally safe. This guide walks through what's actually safe by default, what isn't, and the specific settings that close the gap in each.
Why is XXE classified as a misconfiguration rather than an injection flaw?
XXE is classified as a misconfiguration because the vulnerable behavior is built into standards-compliant parser defaults, not introduced by a developer writing unsafe code. The XML 1.0 specification allows a DOCTYPE declaration to define custom entities, including external ones resolved via a SYSTEM or PUBLIC identifier pointing at a file path or URL — a compliant parser that honors this is doing exactly what the spec says. OWASP's move of XXE from its own A4:2017 category into Security Misconfiguration — A05:2021, and now A02:2025 in the current Top 10 — reflects that the actual defect is almost always "the team never changed the parser's default DTD handling," not a missing input filter. That framing matters for remediation: there's no sanitization function that fixes XXE the way escaping fixes XSS. The only durable fix is configuring the parser itself to refuse external entity resolution, which is why this is a checklist problem — one that has to be verified per language, per library, and often per parser instance in a codebase, rather than a single code review finding.
What's the actual fix, and does it also stop denial-of-service attacks?
The fix, per OWASP's XXE Prevention Cheat Sheet, is to disable DTD processing outright wherever an application has no legitimate reason to accept a DOCTYPE — in Java, that's factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) on the relevant DocumentBuilderFactory, SAXParserFactory, or XMLInputFactory. If a real use case requires DTDs (some legacy document formats do), the fallback is to explicitly disable external general entities, external parameter entities, and XInclude processing while leaving internal DTD structure intact. This same fix closes two vulnerability classes with one setting: disabling DTD processing prevents both external entity resolution (file/SSRF disclosure) and the "Billion Laughs" entity-expansion denial-of-service, where a DTD defines nested entities that each reference the previous one a handful of times, expanding to gigabytes of memory on parse. Because both attacks route through the same DOCTYPE mechanism, there's no case where you'd want DTD parsing disabled for one and not the other — it's a single control, not two.
Is Python's standard library safe against XXE by default?
No — Python's standard library XML parsers are not safe against entity expansion and external entity resolution out of the box, which is precisely why the community-maintained defusedxml package exists as a drop-in replacement. xml.etree.ElementTree, xml.dom.minidom, and xml.sax all use the same underlying expat parser without hardened defaults, so code that does ElementTree.parse(user_supplied_file) inherits expat's willingness to resolve external entities. defusedxml wraps the same standard-library modules — defusedxml.ElementTree, defusedxml.minidom, and so on — with entity resolution disabled and expansion limits enforced, and its documented guidance is to use it as a straight import substitution rather than rewriting parsing logic. For teams auditing a Python codebase, the practical check is a source-to-sink trace: does untrusted input (an upload, an API body, a webhook payload) reach any xml.etree, xml.dom, or xml.sax call without first passing through defusedxml. That's a dataflow question, not a keyword search, since the vulnerable call and the untrusted source are often in different files.
Have PHP and .NET actually fixed this, or is it still opt-in?
Both ecosystems have moved toward safe-by-default, but at different times and for different reasons. PHP's libxml-backed extensions (SimpleXML, DOMDocument) historically loaded external entities unless a developer explicitly called libxml_disable_entity_loader(true), making XXE a common finding in PHP codebases through the 2010s. PHP 8.0 changed this: external entity loading is disabled by default, so applications built on 8.0+ are protected unless something deliberately re-enables it. .NET closed its equivalent gap earlier — XmlReaderSettings.DtdProcessing has defaulted to Prohibit since .NET Framework 4.5.2, which also hardened XmlDocument when it's constructed through an XmlReader with default settings. The risk in both ecosystems now concentrates in old code: a PHP 7.x codebase migrated to 8.x without removing a lingering libxml_disable_entity_loader(false) call, or .NET code that explicitly sets DtdProcessing.Parse for a legacy integration and never revisits it. Version alone isn't proof of safety — it's proof the default is safe, which an explicit override can still undo.
What does a real-world XXE finding look like in production software?
IBM Tivoli Workload Scheduler carried an XXE vulnerability affecting versions 9.4, 9.5, and 10.1, documented in NVD, illustrating that XXE isn't a theoretical bug class confined to hobby projects — it lands in enterprise scheduling and workflow software that parses XML job definitions and configuration files as a core function. The pattern is consistent across XXE CVEs generally: a product accepts XML input (a config import, a job definition, an API payload) through a parser that was never explicitly hardened, and the fix ships as a configuration change to the parser initialization, not a rewrite of business logic. That's a useful signal for prioritizing your own audit — any feature that accepts XML uploads or XML-formatted API requests, especially ones inherited from older integrations or vendored libraries, deserves a direct check of parser settings before assuming XML handling is safe.
How can teams verify their own codebase isn't exposed?
The most reliable check is tracing every point where untrusted input reaches an XML parser and confirming DTD/external-entity processing is explicitly disabled there — not assumed. Safeguard's SAST engine does this as a source-to-sink dataflow trace: it follows untrusted input (a request body, file upload, or webhook payload) through the call graph to parser sinks like DocumentBuilderFactory.newInstance() in Java or ElementTree.parse() in Python, and maps a finding to CWE-611 with the actual trace attached, so a developer sees the exact path rather than a bare "XML parsing detected" warning. For teams that expose XML-accepting endpoints externally, Safeguard's DAST engine complements that by sending safe, non-destructive probes against verified, in-scope targets to confirm whether external entity resolution is actually reachable at runtime — correlating a confirmed dynamic finding back to the source-code sink that produced it. Neither replaces reading your parser's documentation, but together they turn "we think our XML handling is safe" into a verified, per-instance answer.