toolhq.io

All posts
7 min readby Jameel Haider

XXE and XML parser hardening: turning off the features you never wanted

XML has a feature, inherited from SGML, that lets a document define shorthand and expand it during parsing. Those shortcuts can point at external resources. A parser with default settings will fetch them.

The result is that in many languages, handing an XML document to a standard parser gives the document's author the ability to read files off your server and make network requests from it. This is XML external entity injection, and it has been in the OWASP top ten because the defaults were wrong nearly everywhere for a very long time.

How it works

A document type declaration can define entities:

<?xml version="1.0"?>
<!DOCTYPE order [
  <!ENTITY company "Example Ltd">
]>
<order><vendor>&company;</vendor></order>

The parser substitutes &company; with the defined text. Useful, contained, and fine.

An external entity points somewhere else:

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

If the parser resolves external entities, it reads the file and substitutes the contents. If your application then echoes the parsed value back, in a response, a confirmation message or an error, the attacker reads the file.

Swap the scheme and it becomes server side request forgery:

<!ENTITY probe SYSTEM "http://169.254.169.254/latest/meta-data/iam/">

That request originates from your server, inside your network, past your firewall, from an address your internal services trust. Cloud metadata endpoints, internal admin interfaces and unauthenticated microservices are all reachable from there.

A third variant needs no external fetch at all. The billion laughs attack nests internal entities so that expansion grows exponentially:

<!ENTITY a "dos">
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">

Ten levels of that turns a few hundred bytes into gigabytes of memory during parsing. It is a denial of service that costs the attacker one small HTTP request.

Blind XXE

Applications that never echo parsed content are not safe, they are just harder to exploit.

If the parser resolves an external DTD, the attacker can host one that defines a parameter entity which embeds the stolen file contents into a URL, and the parser then requests that URL. The data arrives in the attacker's access log. No output from your application is needed.

Failing that, error messages leak. An entity referencing a nonexistent path can be constructed so the parser's error text includes the contents of another file, and any application that returns parser errors to the caller hands that over.

The lesson is that the fix is at the parser, not at the output. Deciding you are safe because you do not reflect the parsed document is a bet that loses.

The fix

Disable document type declarations entirely. Not just external entity resolution, the whole DTD. Almost no application that consumes XML over an API needs a DTD, and disallowing them removes external entities, parameter entities and entity expansion in one setting.

PlatformSetting
Java, DocumentBuilderFactory / SAXParserFactorysetFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
Java, XMLInputFactory (StAX)setProperty(SUPPORT_DTD, false) and IS_SUPPORTING_EXTERNAL_ENTITIES, false
.NET, XmlReaderSettingsDtdProcessing = DtdProcessing.Prohibit
PythonUse defusedxml in place of xml.etree, minidom or lxml
PHPlibxml 2.9 and later do not load external entities by default; do not re-enable it
Go, encoding/xmlDoes not expand external entities; no action needed
Node, libxmljsDo not pass noent: true or nonet: false
Ruby, NokogiriDefault is safe; do not pass NOENT

Two notes on that table. Java is the worst offender historically, because the defaults in DocumentBuilderFactory resolve external entities and there are many factory classes each needing their own configuration. If you write Java and parse XML, audit every factory instantiation in the codebase rather than assuming a shared helper covers it.

Python's standard library parsers are vulnerable to the expansion attacks even where external entity loading is off, which is why defusedxml exists as a drop in replacement. Installing it and changing the import is a one line fix per module.

XMLConstants.FEATURE_SECURE_PROCESSING in Java limits expansion but does not stop external entity resolution on its own. Set the explicit features.

Where XML hides

Teams conclude they are not affected because their API is JSON. XML is more widely embedded than that suggests:

  • SVG is XML. Any feature that accepts an uploaded SVG and processes it server side, including thumbnail generation and sanitisation, is parsing attacker controlled XML.
  • Office documents. DOCX, XLSX and PPTX are zip archives of XML. So is ODF.
  • SOAP, obviously, and any legacy integration still speaking it.
  • SAML assertions, which are XML, signed, and central to single sign on.
  • RSS and Atom feeds, if you ingest them.
  • XML sitemaps, if you crawl them.
  • SVG and XML in PDF metadata, if you process uploads.
  • Configuration files parsed at startup, which are lower risk but still worth hardening.

There is also a content type confusion case worth checking. Some frameworks content negotiate on the request body, so an endpoint documented as JSON only will happily parse a body sent as Content-Type: application/xml. That turns an XXE into a vulnerability in an API nobody thought was XML at all. Testing it is straightforward: send an XML body with an entity to your own JSON endpoint and see what comes back. The API tester lets you set an arbitrary content type and body against your own endpoints and read the raw response, which is the quickest way to find out.

XInclude and XSLT

Disabling DTDs closes the entity route. Two related features can reopen it.

XInclude is a separate mechanism for pulling in external content, enabled by a processor setting rather than by the document's DTD. If you have turned XInclude on, an attacker can use it to fetch files without a DOCTYPE at all. Leave it off.

XSLT is a full transformation language, and some processors expose extension functions that read files or execute code. If your application applies an attacker supplied stylesheet, the DTD question is the least of the problem. Never process a stylesheet from an untrusted source.

Verifying your own documents

When you are working on XML by hand, whether an SVG, a feed or a config file, you want to inspect and format it without handing it to a service that will parse it under unknown settings. The XML formatter runs in the browser and never transmits the document, which matters when the file you are debugging contains credentials or customer data. For checking that a suspicious document is what it claims to be, the diff checker will show what changed between a known good file and the one you received.

A short checklist

  1. Disable DTD processing in every XML parser in the codebase. Search for every parser and factory instantiation, not just the ones in the obvious code path.
  2. Leave XInclude off, and never process untrusted stylesheets.
  3. Set size and depth limits on anything you parse, and a timeout, so an expansion attack that gets through a misconfiguration fails fast.
  4. Do not return parser error text to callers. Log it, return a generic message.
  5. Restrict outbound network access from services that parse XML, so a successful SSRF reaches nothing useful.
  6. Check whether your JSON endpoints accept an XML body. Many do.

Related reading: XML vs JSON covers the format tradeoffs, what is a reverse proxy covers the network boundary an SSRF crosses, and share logs and API keys safely covers handling the kind of file that turns up in these investigations.