XML Tutorial · XML in Code · Lesson 22 of 28

XML Security: XXE and Entity Attacks

XML external entity (XXE) attacks, billion-laughs entity expansion, SSRF through DTDs, and safe parser settings in Python, PHP, Java and .NET.

Create a free account to save this page, take notes and track your progress. Sign in

Why XML parsers are an attack surface

A DTD can declare entities, and an entity can refer to an external resource:

<!DOCTYPE order [ <!ENTITY secret SYSTEM "file:///etc/passwd"> ]>
<order><note>&secret;</note></order>

If the parser expands it, the file's contents end up in the document, and often in an error message or response shown to the attacker. Variants:

Attack What happens
XXE file disclosure SYSTEM "file:///…" reads local files
SSRF SYSTEM "http://169.254.169.254/…" makes the server call internal URLs (cloud metadata!)
Billion laughs Nested entities expand a 1 KB document into gigabytes (denial of service)
Out-of-band XXE Parameter entities send data to the attacker's server

XXE was a separate category in the OWASP Top 10 (2017) and is still found in SOAP services, SAML, file uploads (DOCX, SVG) and RSS importers.

See it happen, safely

These examples create a harmless local file and parse a document that tries to read it. Each runtime's default settings are shown first.

Python with lxml: since lxml 5 the default parser expands only internal entities, so this document is rejected. Turning resolve_entities on re-enables the attack:

Python (lxml)Try it yourself
from lxml import etree

with open("secret.txt", "w") as f:
    f.write("db_password=hunter2")

attack = b"""<!DOCTYPE order [ <!ENTITY secret SYSTEM "secret.txt"> ]>
<order><note>&secret;</note></order>"""

def note(parser=None):
    try:
        return repr(etree.fromstring(attack, parser).findtext("note"))
    except etree.XMLSyntaxError as e:
        return f"rejected ({e.msg})"

print("default parser:", note())
print("resolve_entities=True:", note(etree.XMLParser(resolve_entities=True)))
print("hardened parser:", note(etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)))

Output

default parser: rejected (Entity 'secret' not defined, line 2, column 22)
resolve_entities=True: 'db_password=hunter2'
hardened parser: ''

PHP: libxml2 doesn't load external entities unless you pass LIBXML_NOENT (a dangerously misleading name: it means "substitute entities"):

PHPTry it yourself
<?php
file_put_contents("/tmp/secret.txt", "db_password=hunter2");
$attack = '<!DOCTYPE order [ <!ENTITY secret SYSTEM "file:///tmp/secret.txt"> ]>
<order><note>&secret;</note></order>';

$doc = new DOMDocument();
$doc->loadXML($attack);
echo "default: '", $doc->getElementsByTagName("note")->item(0)->textContent, "'\n";

$doc = new DOMDocument();
$doc->loadXML($attack, LIBXML_NOENT);
echo "LIBXML_NOENT: '", $doc->getElementsByTagName("note")->item(0)->textContent, "'\n";

Output

default: ''
LIBXML_NOENT: 'db_password=hunter2'

Java: DocumentBuilderFactory does resolve external entities by default. Disallow DTDs entirely:

Java
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.nio.file.*;

public class Main {
    static String parse(DocumentBuilderFactory factory, String xml) {
        try {
            return "'" + factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes("UTF-8")))
                .getElementsByTagName("note").item(0).getTextContent() + "'";
        } catch (Exception e) {
            return "rejected: " + e.getMessage();
        }
    }

    public static void main(String[] args) throws Exception {
        Files.writeString(Path.of("secret.txt"), "db_password=hunter2");
        String attack = """
            <!DOCTYPE order [ <!ENTITY secret SYSTEM "secret.txt"> ]>
            <order><note>&secret;</note></order>""";

        System.out.println("default: " + parse(DocumentBuilderFactory.newInstance(), attack));

        DocumentBuilderFactory safe = DocumentBuilderFactory.newInstance();
        safe.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        safe.setXIncludeAware(false);
        safe.setExpandEntityReferences(false);
        System.out.println("hardened: " + parse(safe, attack));
    }
}

Output

default: 'db_password=hunter2'
hardened: rejected: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.

Run this on your computer — see “Get Started” for setup.

Billion laughs: modern libxml2 detects runaway expansion and stops:

Billion laughsTry it yourself
from lxml import etree

levels = ['<!ENTITY lol0 "lol">'] + [
    f'<!ENTITY lol{i} "{("&lol" + str(i - 1) + ";") * 10}">' for i in range(1, 10)]
bomb = f"<!DOCTYPE bomb [{''.join(levels)}]><bomb>&lol9;</bomb>".encode()
print(f"document size: {len(bomb)} bytes, would expand to {3 * 10 ** 9:,} characters")
try:
    etree.fromstring(bomb, etree.XMLParser(resolve_entities=True))
except etree.XMLSyntaxError as e:
    print("stopped:", e.msg)

Output

document size: 750 bytes, would expand to 3,000,000,000 characters
stopped: Maximum entity amplification factor exceeded, see xmlCtxtSetMaxAmplification., line 1, column 7

Safe settings checklist

Language Safe setup for untrusted XML
Python defusedxml for the standard library; lxml with resolve_entities=False, no_network=True (the defaults since lxml 5 are safer)
PHP Never pass LIBXML_NOENT or LIBXML_DTDLOAD for untrusted input
Java factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true), plus FEATURE_SECURE_PROCESSING
.NET XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit } (the default since .NET 4.5.2 for most APIs)
JavaScript Browsers don't load external entities; in Node, disable entity processing in your library

The simplest rule: if you don't need DTDs, reject any document that contains a <!DOCTYPE. See the OWASP XXE Prevention Cheat Sheet.

Test yourself

What is the most robust defence against XXE when your application doesn't need DTDs?

  1. Reject or disable DOCTYPE declarations entirely
  2. Escape the input with htmlspecialchars
  3. Validate against an XSD after parsing
  4. Use HTTPS
Show answer

Answer: Reject or disable DOCTYPE declarations entirely

Entities are declared in the DTD; disallowing DOCTYPE removes the attack at its source, before parsing.

Was this page helpful?