XXE: The Complete Practical Guide to XML External Entity Injection

XInclude Attacks

How XInclude XXE works when you cannot add a DOCTYPE: the xi:include namespace, reading a file with parse=text, and how it differs from classic entity XXE.

Published

Classic XXE needs you to control the top of the document so you can add a DOCTYPE and declare an external entity. Often you cannot: the server builds the XML and only slots your input into one sub-element. XInclude is the answer. It works at the element level, so a single injected element reads a file with no DTD and no entity declaration at all, and it frequently survives the exact defenses that kill entity-based XXE.

When you can't add a DOCTYPE

The DOCTYPE and its DTD only exist at the very start of an XML document. If your input is placed inside a document the application already constructed, like a value dropped into a <search> or <item> element on the server, there is nowhere for you to put a DTD. Entity-based XXE is off the table because you cannot declare the entity. This is a common real-world shape, and it is exactly where XInclude shines.

The XInclude namespace and xi:include

XInclude is a separate XML feature for pulling one document into another. You declare its namespace on an element you control and add an <xi:include> that points at a file:

<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>

The xmlns:xi attribute declares the XInclude namespace, and the <xi:include> element tells the parser to fetch the href and splice it in. No DOCTYPE, no <!ENTITY>, just one element.

Reading a file with parse="text"

The parse attribute decides how the included resource is interpreted:

  • parse="xml" (the default) treats the target as XML, which fails on anything that is not well-formed, the same special-character problem as in-band entity reads.
  • parse="text" pulls the file in as raw text, so it reads cleanly even when the file contains <, >, or &.

For file disclosure you almost always want parse="text". If the included file has an unusual encoding, wrap it with an encoding hint so the text comes back intact.

Differences from classic entity XXE

XInclude is worth understanding as its own thing:

  • It needs no DOCTYPE, so it works when you only control a fragment.
  • It uses element-level syntax, not DTD declarations.
  • It is gated by a different parser setting (XInclude processing) than DTD and entity handling.

That last point is why XInclude is so useful: an application that disabled DTD processing to stop XXE has often left XInclude enabled, so the "fixed" parser is still exploitable through this path.

Where XInclude is enabled

XInclude support varies by parser and is a separate toggle, so try it whenever entity-based XXE is blocked or impossible. Java's XML stack and libxml-based parsers commonly support it, and some document-processing pipelines enable it deliberately to compose files. When your input lands in a server-built document and a DOCTYPE is not an option, an <xi:include> is the move.

XInclude fills the gap classic XXE leaves. To turn the parser toward the internal network instead of the filesystem, see SSRF via XXE. To shut all of this down, see preventing XXE. Practice an XInclude read on a free live target.

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