PlatformPortSwigger Web Security Academy
TopicXML external entity (XXE) injection
LabExploiting XInclude to retrieve files
DifficultyPractitioner
GoalRead /etc/passwd from the server
ToolsBurp Suite (Proxy + Repeater)

When you don’t control the whole document#

Every other XXE in this series injected a full XML document, so we could add our own DOCTYPE and declare entities. Here we can’t. The request body is ordinary form data (productId=1&storeId=1), and the server takes those values and builds the XML itself before parsing it. We only control a value inside a document someone else wrote, so there’s no place to put a DOCTYPE, and entity-based XXE is off the table.

XInclude is the way in. It’s a feature that lets one XML document pull in content from elsewhere, and crucially it works on a sub-part of a document, no DOCTYPE required. If the server places our input into its XML and parses the result with XInclude enabled, we can make it include a local file.

Finding the injection point#

The Check stock feature sends POST /product/stock with productId and storeId as form parameters (note Content-Type: application/x-www-form-urlencoded, not XML). That’s the tell: the value is being embedded into XML server-side rather than sent as XML. Capture the request and send it to Repeater.

Exploitation: an XInclude payload#

Replace the productId value with an XInclude that pulls in /etc/passwd as text:

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

Piece by piece:

  • <foo xmlns:xi="http://www.w3.org/2001/XInclude"> declares the XInclude namespace on a wrapper element, so the parser recognizes the xi:include tag.
  • <xi:include .../> is the include instruction itself.
  • parse="text" tells it to pull the target in as plain text rather than parsing it as XML (so /etc/passwd, which isn’t valid XML, comes through verbatim).
  • href="file:///etc/passwd" is the local file to include.

Send it. The productId is echoed back in the “Invalid product ID” error, and the error now contains the file:

Burp Repeater with the XInclude payload in productId and the response leaking /etc/passwd in the Invalid product ID error
The response: ‘Invalid product ID: root:x:0:0:…’ — XInclude pulled /etc/passwd into the server-built XML, and it’s reflected back. Lab solved.

The app complains about an invalid product ID, but the “ID” it’s echoing is the contents of /etc/passwd. Arbitrary file-read confirmed, and the lab is solved.

Real-world impact#

The impact is the same as entity-based XXE, arbitrary file-read as the application user, but XInclude reaches cases a DOCTYPE can’t:

  • Hits partial-document sinks — anywhere user input is slotted into a larger server-side XML document (SOAP bodies, back-end service calls), XInclude can read files even though you never control the XML prolog.
  • Reads secrets and source — .env files, framework config, database credentials, cloud tokens, and SSH private keys like ~/.ssh/id_rsa.
  • Pivots to SSRF — swap the file:// URL for http:// and the include fetches a URL instead (see the XXE-to-SSRF lab ).

Only ever against systems you are authorized to test.

Remediation#

  • Disable XInclude processing in the XML parser unless the application genuinely needs it. It is off by default in many parsers but explicitly enabled in some setups, turn it off.
  • Disable DTDs and external entities too, the same hardening that stops entity-based XXE.
  • Keep user input out of server-side XML, or sanitize it so markup characters can’t introduce new elements.

Key takeaways#

  • When you only control a value inside a server-generated XML document, you can’t declare a DOCTYPE, so reach for XInclude instead of entities.
  • parse="text" is what lets you read non-XML files like /etc/passwd cleanly.
  • The fix is the same family as any XXE: disable the dangerous parser features (XInclude, DTDs, external entities) you don’t need.