Exploiting XXE to Retrieve Files

Table of Contents
| Platform | PortSwigger Web Security Academy |
| Topic | XML external entity (XXE) injection |
| Lab | Exploiting XXE using external entities to retrieve files |
| Difficulty | Apprentice |
| Goal | Read /etc/passwd from the server |
| Tools | Burp Suite (Proxy + Repeater) |
What is XXE?#
XML external entity (XXE) injection happens when an application parses XML we control, and the parser (the library that reads the XML and turns it into data the app can use) is allowed to process external entities. An entity in XML is a named placeholder, declared in a DTD (Document Type Definition) and referenced in the document; an external entity takes its value from a URI the parser fetches. If we can declare our own entity that points at a local file, the parser reads that file and we can often get the contents reflected back.
To solve this lab we point an external entity at /etc/passwd and have the app
echo it to us.
The target feature#
The shop has a product page with a Check stock feature. Open a product:


Used normally, it returns a plain unit count:

Finding the injection point#
Intercept the request in Burp. Check stock fires a POST /product/stock
whose body is XML, not form data:

<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
<productId>1</productId>
<storeId>1</storeId>
</stockCheck>That first line, <?xml version="1.0" encoding="UTF-8"?>, is the XML declaration:
it just states the XML version and character encoding for the parser. Any endpoint
that parses XML we supply is worth testing for XXE. The next step is to declare our
own entity and see whether the parser resolves it.
Exploitation: reading /etc/passwd#
Add a DOCTYPE that declares an external entity, then reference that entity where
its value will be reflected. The productId value is echoed back in the app’s
“Invalid product ID” error, so that is where we place the entity:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>Piece by piece:
<!DOCTYPE test [ ... ]>adds an inline DTD, the section of an XML document where entities are declared. It has to come before the root element (<stockCheck>), because the document type declaration is only valid in the prolog, before the single root element opens.<!ENTITY xxe SYSTEM "file:///etc/passwd">declares an external entity namedxxe.SYSTEMplus afile://URI tells the parser to load the entity’s value from that local file. Which URI schemes work (file://,http://,php://, and so on) depends on the parser and the platform it runs on.&xxe;references the entity. When the parser expands it, it substitutes the contents of/etc/passwdin that spot.- Because
&xxe;sits inside<productId>, the file contents land where a product ID is expected, and the app reflects them straight back in its error message.
Send it, and the response leaks the file:

The 400 Bad Request complains it’s an “Invalid product ID,” but the ID it’s
complaining about is the full contents of /etc/passwd. Compared with the normal
check earlier, which returned a plain unit count, seeing the file where that number
used to be proves the content came from our entity, not from the app. Arbitrary
file-read confirmed, and the lab is solved.
Real-world impact#
Reading /etc/passwd is a safe stand-in; the underlying primitive (the raw
capability this bug gives us) is arbitrary file-read as the application user.
In a real, authorized engagement that reads whatever the app can:
- Secrets and source —
.envfiles, framework config, database credentials, cloud tokens, and SSH private keys like~/.ssh/id_rsa. - A pivot to SSRF — SSRF (server-side request forgery, making the server send
requests to targets you choose) follows by swapping the
file://URI forhttp://, so the parser fetches URLs for you, reaching internal-only services and cloud metadata (see the XXE-to-SSRF lab ). - Denial of service and sometimes RCE — a small document can use recursive entity expansion (the “billion laughs” attack, where nested entities blow up to gigabytes and exhaust memory), and on some stacks XXE chains all the way to RCE (remote code execution).
Only ever against systems you are authorized to test.
Remediation#
- Disable DTDs and external entities in the parser. This is the single most effective fix; almost no application legitimately needs them.
- Prefer simpler formats. If the endpoint doesn’t need XML, accept JSON instead and drop the XML parser entirely.
A concrete example, parser-specific (here Java’s built-in
DocumentBuilderFactory), turning off document-type declarations, which defeats
entity-based XXE outright:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Disallow DTDs entirely — the most robust XXE defense
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(input);The exact API differs by language and library, but the idea is the same: tell the
parser not to process DTDs or external entities. If DTDs can’t be disabled, at
minimum disable external general and parameter entities
(external-general-entities and external-parameter-entities).
Key takeaways#
- Any endpoint that parses XML you supply is an XXE candidate; test it by declaring an entity and seeing whether it resolves.
- Point the entity reference at a field that is reflected in the response, so the file contents come back to you.
SYSTEM "file:///..."reads local files; swapping it forhttp://turns the same bug into SSRF.