DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

XXE in Document-Processing APIs: The Attack Surface Nobody Hardens

An attacker uploads an Excel file to a reporting API. The server validates the .xlsx extension, checks the ZIP magic bytes, and routes the file to the document parser. Inside xl/workbook.xml, three lines of DOCTYPE declare an external entity pointing to 169.254.169.254. Forty seconds later, the attacker's DNS server logs a callback carrying the IAM role credentials.

The problem is not in the upload handler. It is in the assumption that a spreadsheet is binary.

The Classification Error: File Uploads Treated as Binary Instead of XML

XLSX, DOCX, and PPTX are ZIP archives containing XML. The ECMA-376 specification defines this: xl/workbook.xml holds the spreadsheet structure, word/document.xml holds the document body. The library that opens these files calls an XML parser, and XML parsers resolve external entities by default.

SVG follows the same logic. It is XML by W3C specification. Any API that renders or converts SVG is, by definition, an XML pipeline processing attacker-controlled input.

4Armed documented the XLSX attack vector in detail: unzip the file, inject a DOCTYPE into xl/workbook.xml, rezip and upload. Cell-level input validation, extension allowlists, and ZIP signature checks do not touch the internal XML content.

The developer who refuses to deploy an unauthenticated XML endpoint accepts the same surface when it arrives embedded in a .docx file. The format changes. The risk does not.

Four Document API Patterns That Ship XXE by Default

Apache POI (Java). WorkbookFactory.create() internally calls a SAXParser without setting setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). External entity resolution is active on the first call, with no explicit configuration from the developer.

python-docx and lxml (Python). lxml.etree resolves external entities by default. Passing XMLParser(resolve_entities=False, no_network=True) to etree.parse() is the caller's responsibility. Neither library configures the parser securely without explicit instruction.

ImageMagick and librsvg (PHP/system). ImageMagick delegates SVG processing to librsvg, which can follow external entity URIs on outbound network connections. The image conversion call becomes an unauthorized HTTP proxy for any URL the server can reach.

wkhtmltopdf. Converts HTML to PDF and renders inline SVG and XML. The conversion chain does not require a direct XML endpoint: the embedded document carries the entity, and the converter executes the request during PDF generation.

Three High-CVSS CVEs Where Parser Defaults Were the Root Cause

CVE-2024-34102 (CVSS 9.8, Adobe Commerce). Unauthenticated XXE via the REST API endpoint. The official description called it "improper management of nested deserialization." The actual mechanism is more direct: the backend XML parser processed external entities declared in the request body. Attackers actively exploited this against thousands of merchants in 2024 with no credentials required.

CVE-2025-2905 (CVSS 9.1, WSO2 API Manager). XXE in the API gateway itself due to improper XMLParser configuration. No authentication is required. Affects versions 2.0.0 through 4.2.0. The official WSO2 mitigation disables external DTDs via JVM system properties at gateway startup.

CVE-2024-2374 (CVSS 7.5, WSO2 API Manager). Unauthenticated read of confidential files or HTTP-reachable resources. This is the previous XXE cycle in the same product. Two distinct XXE CVEs in the same gateway across consecutive years indicate a systemic pattern, not a one-off mistake.

CVE-2024-30043 (CVSS 6.5, Microsoft SharePoint). XmlSecureResolver created an unrestricted access policy for file:// URIs through URL parsing confusion between two internal components. Even code written to prevent XXE fails at the policy level when individual controls are not integrated with each other.

Blind XXE OOB: Exfiltrating Data When the API Returns Nothing

Document-processing APIs rarely return raw XML. They return the conversion output: a PDF, cell values, rendered HTML. This makes standard reflected XXE file read impractical and OOB exfiltration the only viable path.

The two-stage technique works as follows: the entity in the uploaded document triggers an HTTP or DNS request to an attacker-controlled server. That server hosts an external DTD that reads the target file and exfiltrates the content via a base64-encoded DNS subdomain. DNS queries cross most corporate firewalls that block outbound HTTP and HTTPS traffic.

H1 #897244 (Zivver): the profile image upload API processed SVG. The HTTP response contained no XML. XXE was confirmed via an SSRF callback to an internal URL, with no reflection in the response. H1 #347139 (Rockstar Games): the emblem editor API processed SVG and exposed LFI and SSRF via XXE, the same chain applicable to cloud metadata endpoints. H1 #1321070 (Adobe AEM Forms): the document conversion service exposed XXE with RCE potential.

All three reports share the same property: the API returned no XML to the client, but the parser continued resolving external entities on the backend.

Cloud Metadata as the SSRF Target: 169.254.169.254 via Document XXE

The AWS instance metadata endpoint at 169.254.169.254/latest/meta-data/iam/security-credentials/<role> returns temporary access keys with no authentication to any process running on the instance. A document parser running on the server is a process running on the instance.

The payload is direct:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>
Enter fullscreen mode Exit fullscreen mode

Injected into xl/workbook.xml or an SVG file, this payload executes during parsing. IMDSv1 returns the response directly via GET. IMDSv2 requires a PUT request with the X-aws-ec2-metadata-token-ttl-seconds header to retrieve a session token first. This blocks the simple GET approach and acts as a partial mitigation. H1 #347139 demonstrated that the LFI and SSRF chain via XXE works on real production platforms.

GCP and Azure expose equivalent endpoints. The address 169.254.169.254 is the standard target, but GCP also responds at metadata.google.internal.

Per-Stack Hardening: Disabling External Entities in Document Parsers

The fix is not to disable document uploads. It is to configure the XML parser before it touches any input, which requires knowing which parser the document library calls internally.

Java (Apache POI + SAXParser):

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Enter fullscreen mode Exit fullscreen mode

Python (lxml / python-docx):

from lxml import etree

parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False
)
tree = etree.parse(file_input, parser)
Enter fullscreen mode Exit fullscreen mode

Use defusedxml as a drop-in safe replacement with secure defaults, requiring no manual parser configuration.

PHP (libxml2):

// PHP >= 8.0
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET); // PHP 8.0+ disables external entity loading by default; LIBXML_NONET blocks network access

// PHP < 8.0
libxml_disable_entity_loader(true);
Enter fullscreen mode Exit fullscreen mode

The MAGO team tool (mago.team) identifies endpoints that accept document uploads and tests whether the underlying parsers process external entities. The tester does not need to know each file's internal format.

The XXE flaw in document pipelines is one missing line of configuration. Nobody writes it because nobody expects an XML parser to run when a spreadsheet loads. Every document conversion pipeline is an XML endpoint. Treating it as such is where the fix starts.

Top comments (0)