Wholesalers import stock by pasting an XML list into the supplier portal. The parser reads the document’s entity declarations and resolves them, including ones that point at a file on disk. Declare an entity that references a system file, use it inside an item, and the file comes back in the import. This is XML external entity injection, or XXE.
New to this? What is XXE?
XML can declare entities, shorthands that expand into something else. An
external entity expands into the contents of a URL or a file:
<!ENTITY x SYSTEM "file:///etc/passwd">.
XXE is when a parser resolves those external entities on untrusted input. It turns an XML upload into file disclosure, and sometimes into server-side requests (SSRF) too. The fix is to turn external entity and DTD processing off.
Make sure the shop is running at http://localhost:3000.
Open the supplier portal. No account needed.
1 · Find it
On the supplier portal there is a big text box for pasting a stock list as XML. Paste this in and click Import:
<stock>
<item>Humbugs x150</item>
</stock> The items are echoed back on the page, which means whatever an item resolves to gets shown to you. That is the channel you will read files through.
2 · Exploit it
Declare an external entity in a DOCTYPE and reference it inside an item:
<?xml version="1.0"?>
<!DOCTYPE stock [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<stock>
<item>&xxe;</item>
</stock> Import it, and the “item” comes back as the contents of /etc/passwd.
Swap the path for source code, config, or secrets. On a cloud box you can often point
SYSTEM at an internal URL and reach the metadata service too.
Why it matters: XXE reads any file the server can, which regularly means keys, tokens and source. It is a classic way an innocent-looking upload becomes a full read of the box.
3 · Understand it
The importer reads the entity declarations, fetches whatever they name, and substitutes them into the document before pulling out the items:
// declarations like <!ENTITY xxe SYSTEM "file://..."> are resolved
entities[name] = readFileSync(path, 'utf8'); // reads the referenced file
const body = xml.replace(/&(\w+);/g, (m, n) => entities[n] ?? m); The parser treated a filename in the document as an instruction to go and read it. That is exactly what external entity resolution does when it is left on.
4 · Fix it
Do not resolve external entities, and ideally do not process a DTD at all. With a real parser that means the secure options; here it means never reading a file a declaration names:
// resolves SYSTEM entities from the document
entities[name] = readFileSync(path); // ignore external declarations entirely
// (real parsers: disable DTD / external entities)
parse(xml, { resolveExternalEntities: false }); Most XML libraries resolve external entities by default in older versions, so pin a current one and set the safe options explicitly. Better still, take stock lists as JSON or CSV where there is no DTD to abuse.
The general rule: parse untrusted XML with external entities and DTD processing switched off. If a format has a feature that can read files or make requests, disable it before you accept the format from strangers.