XXE: The Complete Practical Guide to XML External Entity Injection

How to Prevent XXE

Preventing XXE: disable DTD processing (the one true fix), the per-parser flags for Java, .NET, PHP, and Python, and why input filtering does not work.

Published

Every attack in this cluster comes from one default: an XML parser that resolves external entities. Preventing XXE is about turning that default off at the parser, consistently, everywhere XML is parsed. The good news is that there is a single fix that kills every variant, and it is a configuration change, not a rewrite.

Disable DTD processing entirely

The one true fix: configure the parser to refuse any DOCTYPE. External entities, parameter entities, the nested declarations used for out-of-band exfiltration, all of them can only be declared inside a DTD. A parser that rejects the DOCTYPE outright leaves nowhere for any of them to exist, so in-band reads, blind OOB, SSRF via XXE, and the error-based variants all fail together. Prefer this over selectively allowing DTD features, because partial configurations are easy to get subtly wrong.

Per-parser flags

The concept is the same across stacks, but the exact call differs. Set these on every XML parser you use:

Java   factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
       (and disable external general + parameter entities, and XInclude, if you cannot disallow DOCTYPEs)
 
.NET   XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit;
       (and XmlResolver = null)
 
PHP    libxml 2.9+ (PHP 8+) disables external entity loading by DEFAULT, so just do not set LIBXML_NOENT.
       libxml_disable_entity_loader() is deprecated in PHP 8 and no longer needed; on PHP < 8 call
       libxml_disable_entity_loader(true). On libxml >= 2.13 / PHP 8.4+ you can pass the LIBXML_NO_XXE flag.
 
Python Use the defusedxml library instead of the stock parsers, which patches the XXE-prone behavior.

The recurring mistake is configuring one parser and missing another: the SOAP stack, the document importer, the SAML handler. Audit every place XML is read.

Disable external entities and external DTD loading

If a library genuinely cannot disable DOCTYPE processing (some require DTDs for validation), fall back to the next layer: disable external general and parameter entities, and disable loading external DTDs over the network. This removes the SYSTEM-fetch capability even when a DOCTYPE is allowed. It is a weaker position than refusing DTDs, so use it only when you must.

Do not rely on input filtering

Trying to block XXE by stripping <!ENTITY> or SYSTEM from the input is a losing game:

  • Encodings and alternate representations slip past string matches.
  • The content-type switch reaches the parser through a different path.
  • XInclude needs no entity declaration at all, so an entity blacklist does nothing against it.

Filtering also breaks legitimate XML that happens to contain those strings. Fix the parser, not the input.

Prefer safer formats and libraries

The strategic defenses:

  • Prefer JSON where you have the choice. It has no entity or DTD mechanism, so the entire class of bug does not exist.
  • Use XML libraries that are safe by default. Modern parsers increasingly ship with external entities disabled, and defusedxml exists precisely to make the Python parsers safe without per-call configuration.
  • Re-check after upgrades. A library change or a re-enabled flag (someone adds LIBXML_NOENT for a feature) can quietly reintroduce XXE, so keep parser configuration under review.

The defensive summary is short: disable DTD processing on every XML parser, never trust input filtering, and prefer formats and libraries that are safe by default. To understand exactly what you are defending against, go break it on a free live target.

← Back to XXE: The Complete Practical Guide to XML External Entity Injection