Stopping the Billion Laughs Attack in Your XML Parser
A 547-byte file that expands to 30 million characters. How lxml, ElementTree and Java refused it, what the limits are, and the settings that keep them on.

The billion laughs attack is the oldest denial-of-service trick in XML, and it works with no network and no file access. It uses nothing but entities. Define one entity as a short string. Define a second as ten references to the first. Define a third as ten references to the second. Continue for nine layers, and a document of a kilobyte expands to a billion copies of the string when the parser substitutes them all. The memory and the time are the parser's, so a request of a few hundred bytes can take a server down.
I wrote a small version to see what current parsers do with it.
The file
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
]>
<lolz>&lol7;</lolz>The file is 547 bytes. lol is three characters; each layer multiplies the count by ten, and lol7 is six layers above lol, so the body expands to 3 × 10⁷ = 30,000,000 characters. That is 55,000 times the size of the file, and each added layer would multiply it by ten again.
What the parsers did
file bytes 547 expands to 3 * 10^7 = 30000000 characters
lxml default XMLSyntaxError: Maximum entity amplification factor exceeded, see xmlCtxtSetMaxAmplification., l (0.00s)
lxml huge_tree=True XMLSyntaxError: Maximum entity amplification factor exceeded, see xmlCtxtSetMaxAmplification., l (0.00s)
lxml resolve_entities=False XMLSyntaxError: Maximum entity amplification factor exceeded, see xmlCtxtSetMaxAmplification., l (0.00s)
ElementTree ParseError: limit on input amplification factor (from DTD and entities) breached: line 11, c (0.11s)Every parser refused. lxml uses libxml2, which in the 2.14 line tracks the amplification factor (the ratio of output size to input size) and stops when it passes a threshold; the message names the function that sets the threshold, xmlCtxtSetMaxAmplification. It refused in under a millisecond and refused again with huge_tree=True (which relaxes the limits on depth and text size) and with resolve_entities=False, so those options do not turn this guard off. ElementTree is built on expat, which added an amplification limit in the same spirit, and it stopped after 0.11 seconds. Java did the same on its own terms:
SAXParseException: JAXP00010001: The parser has encountered more than "64000" entity expansions in this document; this is the limit imposed by the JDK.
133 msThe JDK counts entity expansions and stops at 64,000, which this file exceeds long before it gets to 30 million. That limit is a property (jdk.xml.entityExpansionLimit) that a program can raise or set to zero for "no limit". Raising it to make a legitimate big document parse is how the protection gets lost, so leave it.
The interesting number is the time. A parser without a guard would have had to build 30 million characters for this seven-layer file, and billions for the nine-layer one; I did not run an unguarded parser to measure it. With the guards, the cost of the attack is a millisecond.
Why current libraries are safe and you should not rely on it
These results are good news and should be read narrowly. They describe one shape of attack on current versions. The history of this class is that each guard is added after an attack gets through: the plain exponential form was the first, then the quadratic form (one big entity referenced many times, which grows slowly enough to evade ratio checks), then variants using external entities, parameter entities or attributes. Guards differ between libraries and versions, and an application that bundles an old runtime, an old PHP or a vendored copy of expat may have none. Java's limit has been default for years; many older stacks had no limit at all.
So the defense has layers, and the parser's guard is the last one.
Do not accept DTDs. The attack needs a DOCTYPE with entity declarations. A parser configured to reject any document with a DTD is immune to every variant, past and future, and the cost is nothing for data exchange. In Java, that is the disallow-doctype-decl feature; in Python, defusedxml raises EntitiesForbidden; the details are in disabling DTDs and external entities in Java and Python.
Cap the input size before parsing. The attack is small by design, so a size cap does not stop it, but it stops its large cousins: a 2 GB upload that is just big. Read at most N bytes from the request and reject the rest. See limiting XML size and depth for uploads for the numbers.
Cap the time and memory of the process that parses. A worker with a memory limit and a timeout turns a successful attack into one failed job. Run parsers for untrusted input in a separate worker or container, not in the process that serves web requests.
Keep the guards on. Search your code for huge_tree=True, entityExpansionLimit, FEATURE_SECURE_PROCESSING set to false and any option that says "unlimited". Each was added by someone who hit a limit with a legitimate file; the fix then should have been a size review or a streaming parser, not a switch.
Test with the attack. Keep a small billion-laughs file (three layers is enough to show the refusal without risk) in your tests and assert that the parse fails quickly. If a library upgrade or a configuration change removes the guard, the test fails before an attacker finds it.
Telling the cases apart in logs
When a guard fires, the exception message is specific (amplification factor, entity expansions), and it is a security-relevant event, not a data error. Log it with the request source, and alert if it repeats. A feed that legitimately uses entities heavily is rare. A customer whose upload triggers the guard once is likely a mistake; one that triggers it repeatedly is probing.
Related reading
For the overview see XML security problems to close before production, and for the sibling attack, what an XML external entity attack is. Safe parsing in Python is covered in safe XML parsing in Python with defusedxml. The OWASP entry is the XML External Entity Prevention Cheat Sheet, and the classification is CWE-776, improper restriction of recursive entity references. The libxml2 limit is documented in its parser API reference.
When parser limits are not enough
The attack is not entity-based. A deeply nested document, a million siblings or a 100 MB text node does not use entities and passes an amplification guard. Those need depth and size limits (see the next article in this cluster).
The parser is old. If you cannot upgrade, reject DTDs at the application layer by checking for <!DOCTYPE in the first kilobytes before parsing. It is crude, and it works.
You must accept DTDs. Some publishing workflows genuinely use them. Then parse in an isolated worker with strict memory and time limits, a size cap and no network, and treat the guard messages as alerts.
The expansion happens later. Converting XML to another format, rendering it or running an XSLT stylesheet can multiply size after the parse. Limits belong at each stage.


