Choose your stack
Fix XML external entity processing in Python and Flask
Parse untrusted XML with defusedxml, not lxml or the stdlib.
Open the fix card Java · Spring BootFix XML external entity processing in Java and Spring Boot
Configure the XML parser to refuse the DTD: in Java set the Apache feature disallow-doctype-decl to true on the DocumentBuilderFactory, so any DOCTYPE is a fatal error and no external entity (CWE-611) or entity expansion (CWE-776) runs.
Open the fix card C# · ASP.NET CoreFix XML external entity processing in C# and ASP.NET Core
On .NET, parse untrusted XML with XmlReaderSettings.DtdProcessing = Prohibit and XmlResolver = null (both the modern defaults), so any DOCTYPE throws before an external entity is read (CWE-611) or entities expand (CWE-776).
Open the fix cardStacks without a card
- Node.js and Express: no card yet. The nearest is Fix XML external entity processing in Python and Flask.
Why there is no Node.js and Express card
No Node.js XML parser that resolves attacker-controlled entities installs cleanly on Node 26, so a vulnerable-vs-fixed pair cannot be built honestly on this runtime. Verified: libxmljs2 0.37.0 (the libxml2 binding) fails to compile against Node 26.3.0's V8 ABI (node-gyp build error, GetAlignedPointerFromInternalField signature change); fast-xml-parser 5.3.6 does not expand DOCTYPE-declared entities (it keeps '&c;' literal) and throws 'External entities are not supported' (only programmatic addEntity expands), and its entity-expansion flaw CVE-2026-26278 affects only the older 4.1.3-5.3.5 range, which the pinning rule forbids shipping; @xmldom/xmldom 0.9.8 reports 'entity not found' for custom and external entities and resolves neither. That pure-JS XML parsers do not dereference external entities is itself a useful defensive fact: on Node the exposure is overwhelmingly entity-expansion (CWE-776) in older or misconfigured libraries, addressed by keeping the parser current and disabling DOCTYPE processing.
- TypeScript and Next.js: no card yet. The nearest is Fix XML external entity processing in Python and Flask.
- Python and FastAPI: no card yet. The nearest is Fix XML external entity processing in Python and Flask, in the same language.
- Python and Django: no card yet. The nearest is Fix XML external entity processing in Python and Flask, in the same language.
- Go and net/http: safe by default. Go's standard
encoding/xmldecoder does not load external DTDs or resolve external entities: it hands<!DOCTYPE>and<!ENTITY>declarations to the caller as uninterpreted directives and expands only the predefined entities, character references and the fixed strings you put inDecoder.Entity. Another XML parser, such as a binding to libxml2, needs its own check before it reads untrusted XML. Checked for Go 1.25 and 1.26, standard library encoding/xml against Go source: src/encoding/xml/xml.go (go1.26.0) and Go standard library: encoding/xml Decoder. - Ruby on Rails: safe by default. Rails parses only JSON request bodies by default (XML parameter parsing left Rails in 4.0 for the
actionpack-xml_parsergem), and Nokogiri's default XML options,RECOVER | NONET | BIG_LINES, neither substitute entities nor reach the network. Setting Nokogiri'sNOENTorDTDLOADoption, or clearingNONET, on untrusted documents. Keep the REXML gem at 3.3.3 or later for its entity-expansion fixes. Checked for Rails 8.0 and 8.1, Nokogiri 1.19 against Rails guides: upgrading from Rails 3.2 to Rails 4.0, Rails source: action_dispatch/http/parameters.rb (8-1-stable), Nokogiri source: XML::ParseOptions and Ruby security advisory: CVE-2024-41946, DoS in REXML. - PHP and Laravel: safe by default. With libxml2 2.9.0 or later, the minimum every supported PHP release builds against, PHP's XML extensions neither substitute entities nor load external ones unless the code passes
LIBXML_NOENT,LIBXML_DTDLOADorLIBXML_DTDVALID; this is why PHP 8.0 deprecatedlibxml_disable_entity_loader(). libxml2 also limits entity expansion unlessLIBXML_PARSEHUGEis passed. PassingLIBXML_NOENT,LIBXML_DTDLOAD,LIBXML_DTDVALIDorLIBXML_PARSEHUGEwhen parsing untrusted XML. Checked for PHP 8.2 to 8.5 (Laravel 12 and 13) with libxml2 2.9.0 or later against PHP manual: libxml_disable_entity_loader, PHP manual: libxml predefined constants and Laravel 13 release notes: support policy.
How it happens
- A feature parses XML a client controls: a SAML or SOAP message, a document import, an RSS or sitemap fetch, an office-document part, a webhook body. The parser is created with library defaults, and for most Java and .NET-legacy XML parsers the default resolves external entities.
- The attacker sends a DOCTYPE with an external general entity, <!ENTITY x SYSTEM "file:///etc/passwd">, and references &x; in an element the response echoes back. The parser reads the file and inlines its bytes, so the response leaks it (CWE-611). Pointing the entity at http://169.254.169.254/ or an internal host turns the parser into an SSRF client.
- Where the response is not echoed, a parameter entity in the DTD can exfiltrate out of band: it builds a URL from the stolen file and forces the parser to fetch it from an attacker server (the classic error-based or OOB XXE).
- Nested internal entities expand multiplicatively: ten entities each referencing the previous ten turn a few hundred bytes into gigabytes (the 'billion laughs' entity-expansion denial of service, CWE-776). No external access is needed.
- Input-string defences fail. A byte or substring filter that rejects '<!DOCTYPE' or '<!ENTITY' is bypassed by encoding the document as UTF-16 (or UTF-7): the parser honours the encoding declaration and sees the DOCTYPE, while the ASCII filter never matched the bytes.
Fix principles
- Disable the DTD entirely wherever the parser offers it: this is the single most effective control and the one the OWASP cheat sheet lists first. In Java set the Apache feature 'disallow-doctype-decl' to true; in .NET leave (or set) XmlReaderSettings.DtdProcessing = Prohibit; in Python use defusedxml with forbid_dtd=True.
- Where some DTD support is genuinely required, disable the dangerous parts instead: external general entities, external parameter entities, external DTD loading and XInclude, and do not expand entity references. Set each feature off rather than relying on defaults.
- Null out the entity/URL resolver so the parser has no way to fetch anything: XmlResolver = null in .NET, an EntityResolver that throws in Java, no_network=True in lxml.
- Validate where the value is used, not by filtering the input string. Reject the DOCTYPE at the parser; do not try to strip '<!DOCTYPE' from bytes, which a different character encoding defeats.
- Bound the input: cap the request body size, and cap entity expansion (JAXP secure processing, .NET MaxCharactersFromEntities, lxml's built-in limits) so a small document cannot expand without bound.
- Keep parser error text off the response and prefer a data format without a DTD (JSON) when the XML itself is not a requirement.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation passes, and the vulnerable and the plausible-but-wrong (input-filter) implementations fail. run-all.mjs does this for every card.
- Exploit tests use only self-contained inputs: a DOCTYPE whose external entity points at a marker file the test creates in its own temp directory (assert the marker never appears in the response and the parser rejects the DTD), and a small bounded internal-entity expansion a few levels deep (assert it is refused, never a real billion-laughs). Each vector is sent once as UTF-8 and once as UTF-16, which slips past the wrong implementation's byte filter. Functional tests parse an ordinary document, predefined and numeric character references, and non-ASCII text, and answer 400 for malformed or empty input.
- Static analysis is strong at finding the sink and weak at proving a config safe. Each card's scannerRules records what each Semgrep rule, CodeQL query and built-in analyzer (Bandit, .NET CA3075) actually did on the three implementations; several flag the hardened code too, because they match the parser call rather than read its configuration.
References
- OWASP XML External Entity Prevention Cheat Sheet
- OWASP Top 10:2025 A02 Security Misconfiguration (CWE-611 and CWE-776 mapped here)
- CWE-611: Improper Restriction of XML External Entity Reference
- CWE-776: Improper Restriction of Recursive Entity References in DTDs ('XML Entity Expansion')
- CAPEC-221: Data Serialization External Entities Blowup
- CAPEC-197: Exponential Data Expansion
- PortSwigger Web Security Academy: XML external entity (XXE) injection
- W3C XML 1.0 (Fifth Edition): entity declarations and the DTD
Attack patterns:
Practice in the browser
- XXE lab: Parse an uploaded invoice without resolving external entities · Practitioner, Python
Guides on this weakness
Keep untrusted values out of executable syntax
Choose a safe interface for each interpreter and bound the work performed by parsers and converters.
Convert untrusted office documents and PDFs in an isolated parser
DOCX, XLSX and ODF files are ZIP archives of XML, and PDFs can carry scripts; the converters that read them are large C and C++ programs with a long CVE history. Check the package before any parser reads it, convert each document in a container with no network, no writable root and hard limits, kill it at a deadline, and re-render to pixels when people will open the result.
Parse bounded JSON and XML into an explicit application schema
Separate parser capabilities from schema rules, reject ambiguous and coercible inputs, and implement the boundary in a complete Python project.
SAML single sign-on: verify signed assertions without losing the identity boundary
Bind a validated SAML assertion to its trusted issuer, the exact signed element, your service, the browser request and a replay record before creating a local session, and keep your SAML library ahead of its advisories.