skip to content

XML Entity Attacks

Billion laughs, external entity expansion and quadratic blowup aimed at the bundled XML parsers, and the handler flags that switch entity resolution off. Asked because the defaults are not safe.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does xml.etree.ElementTree do with an external entity reference like &xxe;?

level: middleimportance: must knowfreq 42%

answer

  1. Two entity kinds, two different outcomes
  2. Nothing is fetched from disk or network
  3. The parser has no definition for it
  4. ElementTree raises where xml.sax stays quiet
  5. ParseError says undefined entity

basics

~20 s

It refuses it. ElementTree does not process external general entities, so nothing defines that reference and the parse fails with xml.etree.ElementTree.ParseError reading 'undefined entity'. Entities declared inline in the document's DTD subset are still expanded normally.

solid answer

~40 s

It never fetches it. Since Python 3.7.1 external general entities are not processed by default, and `xml.etree.ElementTree` turns that into a hard failure: the reference is undefined as far as the parser is concerned, so `fromstring` raises `xml.etree.ElementTree.ParseError: undefined entity &xxe;`. That is stricter than its siblings — `xml.sax` with `xml.sax.handler.feature_external_ges` at its default of False emits no character data at all and parses cleanly, and `xml.dom.minidom` drops the reference. What ElementTree *does* still do is expand entities declared inline in the internal DTD subset, so `<!ENTITY greet "hello">` really does become `hello`. Runaway amplification of those inline entities is caught one layer down, by libexpat 2.4.1 and newer, and also surfaces as a `ParseError`.

code

python · 26 lines
python
import io
import xml.etree.ElementTree as ET
import xml.sax
from xml.sax.handler import ContentHandler

doc = ('<?xml version="1.0"?>'
       '<!DOCTYPE d [<!ENTITY xxe SYSTEM "file:///etc/hosts">]>'
       '<d>&xxe;</d>')

try:
    ET.fromstring(doc)
except ET.ParseError as exc:
    print("ElementTree:", exc)


class Collect(ContentHandler):
    def __init__(self):
        self.text = []

    def characters(self, content):
        self.text.append(content)


handler = Collect()
xml.sax.parse(io.BytesIO(doc.encode()), handler)
print("xml.sax:", repr("".join(handler.text)))

go deeper

for a junior

Remember the outcome first: Python does not fetch what an external entity points at, and xml.etree.ElementTree raises a ParseError saying the entity is undefined. Nothing from the filesystem or network reaches your tree.

for a middle

Explain the mechanics: external general entities unprocessed by default since 3.7.1, ElementTree's own entity table producing the undefined-entity error, and internal declarations still being expanded as ordinary XML.

for a senior

Show that you know the exception is a side effect, not a policy. Point out that xml.sax returns an empty string for the same document, that broad exception handling erases the signal, and that amplification protection lives in the linked libexpat.

for a principal

Frame it as a boundary decision: one sanctioned entry point for untrusted XML with an explicit no-DTD policy, so behaviour does not vary by which stdlib front-end a team happened to import.

## The two entity kinds are not the same threat, and Python treats them differently - An *external* general entity is declared with a system identifier — `<!ENTITY xxe SYSTEM "file:///etc/passwd">` — and its value is whatever is at that location. Resolving it turns a parser into a file reader and an outbound HTTP client. - An *internal* general entity is declared with a literal value — `<!ENTITY greet "hello">` — and expanding it costs only text substitution, until the declarations reference each other and the substitution becomes exponential. ## What ElementTree actually does with the external one Since Python 3.7.1 the standard library stopped processing external general entities by default. `xml.etree.ElementTree` does not merely skip the reference: it treats it as undefined and **raises**. Parsing `<!DOCTYPE d [<!ENTITY xxe SYSTEM "file:///etc/hosts">]><d>&xxe;</d>` with `xml.etree.ElementTree.fromstring` gives `xml.etree.ElementTree.ParseError: undefined entity &xxe;: line 1, column 79`. The reason is ElementTree's own entity table: 1. `xml.etree.ElementTree.XMLParser` carries an `entity` mapping seeded with the five predefined XML entities, 2. expat hands it an unresolved reference, 3. ElementTree looks the name up, misses, and raises. No filesystem access, no socket, no partial tree. ## That behaviour is not uniform across the front-ends, which is exactly the interview point - `xml.sax` reaches the same safety outcome silently: with `xml.sax.handler.feature_external_ges` at its default of `False`, the parse succeeds and the content handler simply never receives characters for the entity. - `xml.dom.minidom` likewise does not fetch it. So the same hostile document produces a loud exception from one module and an empty string from another — and the empty string is the more dangerous shape in application code, because it looks like a valid parse of an empty field. If you branch on "did parsing succeed", you get different answers depending on which module you happened to use. ## What ElementTree still expands Internal declarations work normally, because they are part of ordinary XML. `<!DOCTYPE d [<!ENTITY greet "hello">]><d>&greet;</d>` parses to an element whose `text` is `hello`. That is the door the classic **amplification document** walks through: nine nested internal declarations, each referencing the previous one ten times, from a payload of a few hundred bytes. On 3.14 that is caught by the linked **libexpat** — 2.4.1 and newer enforce an amplification-factor limit — and it also surfaces to Python as a `ParseError`, this time reading "limit on input amplification factor (from DTD and entities) breached". Note where the protection lives: in C, in a library whose version you did not necessarily choose. `xml.parsers.expat.EXPAT_VERSION` tells you what you actually linked. ## Why "it raises, so I am fine" is only half a correct answer The exception is real protection against the file-disclosure shape, but it is a side effect of ElementTree's entity table rather than a policy you configured. There is no argument to `fromstring` or `parse`, and no attribute on `XMLParser`, that says "reject documents containing a DOCTYPE" or "cap total expanded size". If you need a stated policy, either: - drive `xml.parsers.expat` yourself and raise from an `EntityDeclHandler` — combined with `SetParamEntityParsing` set to `xml.parsers.expat.XML_PARAM_ENTITY_PARSING_NEVER` — - or use a hardened third-party XML library whose parse entry points refuse entity declarations outright. ## How to talk about the fix Say: - what the parser does (refuses external entities, expands internal ones), - where the remaining guard lives (libexpat, version-dependent), - and what you would deploy (a hardened library for untrusted documents, a byte cap before the parse, and stdlib parsers only for XML you generated). An interviewer is checking whether you know that "ElementTree raised an exception on my test payload" is an observation, not a security control. **A detail worth carrying.** Because the failure is a `ParseError`, code that wraps parsing in a broad `except` and returns a default will convert a rejected attack into a silent empty result — the same shape `xml.sax` produces natively. Log the parse error with the document's size and source before you swallow it. ## Two things that are not entity attacks - **Numeric character references** such as `&#65;` are not entity declarations at all — they are ordinary character escapes, always expanded, and never a vector. - And a DOCTYPE that merely names an **external DTD**, as in `<!DOCTYPE d SYSTEM "shared.dtd">`, is not fetched either. The practical consequence catches teams out: a document that legitimately relied on entities declared in that shared DTD now fails with exactly the same `undefined entity` error as an attack payload. When a well-behaved sender's feed stops parsing after an upgrade, that is usually the cause — and the fix is to resolve the shared declarations out of band from a location you control, never to re-enable external entity resolution for everyone.

  • Why does xml.sax parse the same document without raising?
    Because skipping is not the same as rejecting. With `xml.sax.handler.feature_external_ges` at its default of False, expat never resolves the external entity and the reader simply reports no character data for it, so the parse completes normally. Application code then sees an empty field rather than an error, which is why the quiet outcome is often the more dangerous one to build on.
  • Does ElementTree expand entities declared inline in the document?
    Yes. Internal general entities are ordinary XML and are substituted normally, so `<!ENTITY greet "hello">` yields the text `hello`. That is the mechanism amplification attacks abuse, and the only thing stopping a nested-declaration bomb is libexpat's amplification-factor limit from 2.4.1 onwards — a C-level guard, not something you configured in Python.
  • How would you make rejecting a DOCTYPE an explicit policy rather than a side effect?
    Either drive `xml.parsers.expat` directly and raise from an `EntityDeclHandler`, with `SetParamEntityParsing` set to `XML_PARAM_ENTITY_PARSING_NEVER`, or use a hardened third-party XML library whose parse functions refuse DTDs and entity declarations and raise a dedicated exception. Both give you a rule you can test; relying on ElementTree's undefined-entity error gives you a behaviour that could change.

saying these in an interview costs you the question

  • Says ElementTree reads the file the entity points at
  • Claims the reference is returned as literal text
  • Thinks xml.sax raises on the same document
  • Believes internal entity declarations are ignored too
  • Treats the ParseError as a configured security control
  • Wraps the parse in a bare except and returns a default

context

open as a page

Is xml.etree.ElementTree safe for parsing XML supplied by untrusted users?

level: juniorimportance: should knowfreq 30%

basics

~20 s

No. Python's xml package documentation states its parsers are not secure against maliciously constructed data. ElementTree will not fetch external entities, but it still processes a DTD and expands internal entities, so untrusted XML belongs in a hardened parsing library.

open as a page

A chat-transcript archiver using xml.etree.ElementTree on user-supplied exports intermittently blows its 92nd-percentile latency budget — how do you confirm entity expansion is the cause and harden the parse?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Correlate parse time and memory against input size: a bomb is tiny input with huge cost. Catch xml.etree.ElementTree.ParseError and read its message, check xml.parsers.expat.EXPAT_VERSION, then move untrusted parses behind a hardened library, a byte cap, and a killable worker process.

open as a page

In xml.sax, what do feature_external_ges and feature_external_pes control?

level: middleimportance: nice to knowfreq 14%

basics

~20 s

They are the SAX feature identifiers for external general and external parameter entities. Enabling feature_external_ges makes the reader fetch whatever an entity's system identifier points at; the expat-backed reader refuses any attempt to enable feature_external_pes.

open as a page