Billion Laughs: Denial of Service Through XML Entity Expansion
An XML parser that expands internal entities will happily inflate a few hundred bytes into gigabytes of memory. Here is how the billion-laughs bomb and its quadratic-blowup cousin DoS a server with no network and no external DTD, and the parser limits that stop it.
<!ENTITY lol9 "&lol8;&lol8;&lol8;">On this page
Most XXE attacks reach outward — they make the parser read a file or fire a request to somewhere it should not go. The billion-laughs attack does the opposite: it never touches the network and never names an external resource. It hands the parser a tiny, perfectly well-formed document and lets the parser destroy itself. The whole payload is a handful of internal entity definitions, each one referencing the previous a few times over, so that a single top-level reference forces an exponential cascade of substitutions. A few hundred bytes on the wire expands into gigabytes in memory.
This post shows how nested internal entities turn a routine XML endpoint into a denial-of-service primitive, why the quadratic-blowup variant slips past naive nesting-depth checks, and the parser limits that cap expansion before it ever runs away.
How it happens
Anywhere an application parses untrusted XML — SOAP, SAML, document import, webhooks, RSS — the parser must resolve the entities a document declares. Reflected and blind XXE exploit external entities, but entity expansion needs nothing external at all: internal entities are defined and referenced entirely within the DOCTYPE. The vulnerability appears when the parser expands them with no ceiling on how many substitutions it will perform or how large the result may grow. Consider a .NET endpoint that loads an uploaded XML document:
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse }; using var reader = XmlReader.Create(request.Body, settings); var doc = new XmlDocument(); doc.Load(reader); Process(doc); return Results.Ok();
This parses fine for ordinary documents. But DtdProcessing.Parse permits a DOCTYPE, and the document is free to declare internal entities that reference each other. The parser will dutifully expand every reference — including one whose expansion is exponential in the depth of the nesting.
A concrete attack: nested entities that explode
The attacker uploads a document with ten internal entities. lol1 is a short string; every entity after it references the one below it ten times; the body references the top entity once:
<?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;"> <!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;"> <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> ]> <root>&lol9;</root>
Each level multiplies the previous by ten, so &lol9; expands to 10^9 copies of lol — roughly three gigabytes of text from a document under a kilobyte. The parser allocates as it expands; long before the document is "read", the process is thrashing or killed by the out-of-memory reaper. No file was read, no request was sent — the parser DoSed itself resolving an entity the document was perfectly entitled to declare.
The attacker did not flood the server with traffic. They sent one small, valid document and let the parser do the work — the amplification lived entirely inside the entity table, expanding in memory the parser was told to allocate.
The quadratic-blowup variant
Some defenders react to billion-laughs by limiting nesting depth — refuse a document whose entities reference more than a few levels deep. That check misses the quadratic variant, which is shallow by design. Instead of nine layers, it defines one large entity and references it many times at a single level:
<?xml version="1.0"?> <!DOCTYPE bomb [ <!ENTITY a "AAAAAAAAAAAAAAAA... (about 50,000 'A's) ...AAAA"> ]> <root>&a;&a;&a;&a;&a; ... (about 50,000 references) ... &a;</root>
Here nesting depth is one, so a depth limit waves it through. But the parser still expands 50,000 × 50,000 characters — about 2.5 gigabytes — from a payload of roughly 100 KB. The cost is quadratic in the input size rather than exponential, which makes it slower to build but far harder to spot with a structural rule: there is nothing deeply nested to flag, only one entity referenced an unusual number of times. Depth alone is the wrong thing to measure; total expansion is what hurts.
What an attacker gains
Entity expansion is a pure availability attack, and a cheap one:
- Memory exhaustion — a sub-kilobyte request forces multi-gigabyte allocations, crashing the worker or the whole host.
- CPU starvation — building and concatenating the expansion pins a core; a few concurrent requests stall the service for everyone.
- Amplified, low-cost denial of service — the asymmetry is the point: tiny upload, enormous server-side work, no botnet or bandwidth required.
- Evasion of naive guards — the quadratic variant defeats depth checks, so a half-measure leaves the door open.
The fix: cap or disable entity expansion
Like the rest of the XXE family, this has a clean parser-level fix — bound the expansion, and ideally refuse DTDs altogether since the application almost never needs them. In .NET, prohibit DTD processing outright:
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; using var reader = XmlReader.Create(request.Body, settings); var doc = new XmlDocument { XmlResolver = null }; doc.Load(reader);
With DtdProcessing.Prohibit the document is rejected the instant its DOCTYPE appears — there are no internal entities to expand, so neither the exponential nor the quadratic payload can run. The same principle applies on every platform:
- Prefer to disable DTDs entirely (
disallow-doctype-decl,DtdProcessing.Prohibit,resolve_entities=False); if a feature genuinely needs them, enable secure-processing mode and set a hardentityExpansionLimitso the parser aborts once expansion crosses a threshold. - Bound the total expanded size and entity count, not just nesting depth — depth limits miss the quadratic variant entirely.
- Keep XML libraries patched, since secure-processing defaults and expansion ceilings change between versions.
How SelfSec finds it
SelfSec crawls the target, submits documents whose DOCTYPE declares nested and large-reference internal entities sized to be amplifying but safe, and measures the parser's response: a request that should return in milliseconds blowing out into seconds of latency, a memory spike, or a dropped connection is the signal that expansion is uncapped — a confirmed availability finding, not a guess from the presence of a DOCTYPE. The probes are calibrated to demonstrate the blowup without taking the target down, everything runs locally on 127.0.0.1 so your scan data never leaves your machine, and each finding ships with the exact payload and the observed timing so you can replay it.
SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.
Do both things about XML External Entity
SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.