XXE Injection Against Modern XML Parsers

Default XML parsers enable dangerous entity resolution without developers realizing the risk.

Editor at Large · · 8 min read
Cover illustration for “XXE Injection Against Modern XML Parsers”
Exploitation Techniques · September 25, 2026 · 8 min read · 1,893 words

What the parser does with a malicious DOCTYPE declaration

XXE injection is not some relic engineers trip over by accident. It survives into 2026 because the dangerous behavior isn't a bug someone wrote, it's a default someone shipped. OWASP folded XXE into Security Misconfiguration years back, and that category is A02 in the OWASP Top 10:2025. This is a parser doing what it was built to do, out of the box, without a developer forgetting a parameterized query. It's a parser doing what it was built to do, out of the box, with nobody stopping to ask if that's a good idea before it ships.

External entities are a real, documented part of the XML spec. They let a document pull in outside content, such as a file, a URL, or a fragment from another source. Nobody snuck this in as a defect, and it ships turned on in a long list of parser libraries. The vulnerability is something a team inherits the second they call a library's default constructor. It's something they inherit the second they call a library's default constructor. "We didn't write anything dangerous" falls apart as a defense, because the danger was never in the code the team wrote. It was already sitting in the library, waiting for someone to call it.

Get the entity model straight and everything else clicks. A DOCTYPE declaration can define entities, and a SYSTEM identifier tells the parser to go fetch that entity's content from somewhere: a file path, a URL. Once fetched, the parser drops that content right into the document, inline, as if it had been there the whole time.

The classic probe is almost insultingly simple. An attacker writes a DOCTYPE with an external entity pointing at file:///etc/passwd, references that entity somewhere in the body, and sends the document off. If the application's response contains the contents of /etc/passwd, the parser resolved it. That's the whole test. No exploit chain, no obfuscation, just a parser doing what it does.

From there, a resolving parser pushes toward four distinct outcomes. It can read local files and hand back their contents, plain disclosure. It can be told to fire HTTP or DNS requests at arbitrary hosts, turning the parser into an SSRF launchpad. It can pull in remote file content and paste it straight into the document. Or it can choke on a chain of nested entity references that multiply on each other, the Billion Laughs attack: entity lol2 expands to a dozen copies of lol1, lol3 expands to a dozen copies of lol2, and on it goes, until the parser is holding gigabytes of text in memory from a payload that started at a few hundred bytes. No network call needed for that one. The cascade collapses the parser on structure alone.

The escalation chain from file read to confirmed critical

Diagram: XXE Escalation: Four Stages from File Read to Critical. Visualizes: Show a vertical escalation chain of four named stages, each with a short label and outcome, illustrating how an XXE exploit moves from low-stakes probing to catastrophic…

Exploiting XXE rarely stops at the first successful read. It moves in stages, and each stage raises the stakes.

Stage one is basic file disclosure: point the entity at /etc/passwd or something like it, see if it reflects. If it does, the parser trusts external entities, and it's time to push further.

Stage two swaps the file path for an internal URL, turning the entity into an SSRF primitive. The parser, running server-side, makes the request on the application's behalf, walking straight past perimeter firewalls that were never built to stop traffic starting from inside. Cloud metadata endpoints are the real prize here. AWS's 169.254.169.254/latest/meta-data/iam/security-credentials/, and the equivalent paths on Azure and GCP, can leak IAM credentials directly to whoever controls the payload. Internal admin panels and unauthenticated internal APIs, built on the assumption that nothing outside the network can reach them, become reachable the moment a parser will fetch a URL on command.

Stage three is blind XXE. The application never reflects anything back, but the parser still processes the entity, just with no visible output to confirm it happened. Detection moves out-of-band from there: send an entity pointing at a domain under attacker control, then watch for a DNS lookup or an HTTP callback. A hit confirms the parser processed the entity even with nothing showing in the response. Full exfiltration under blind conditions takes one more step: host a malicious external DTD, have it define a parameter entity that reads the target file, then build an HTTP request back to the attacker's server with the file contents baked in (Base64-encoded if the file has characters that would break a URL). There's also an error-based variant, where a deliberately malformed reference triggers a parser error message that happens to include the file's contents.

Stage four is the one testing plans miss most: second-order XXE. The payload gets stored somewhere, through an import feature, a saved document, a queued job, and gets parsed later, asynchronously, disconnected in time from the original submission. Standard entry-point testing checks what happens right after submission. It almost never checks what happens three hours later when a batch job picks the file back up and parses it cold.

WAF evasion via character encoding and filter bypass

A web application firewall inspecting XML traffic is usually only looking at UTF-8. Re-encode the same document as UTF-16LE with a byte order mark, or as UTF-7, set the matching charset in the Content-Type header, and a lot of inspection rules stop recognizing what they're looking at. The underlying XML parser handles the encoding fine and processes the document exactly as intended, filter or no filter.

If a filter blocks the file:// protocol specifically, hosting the malicious DTD externally and referencing it over HTTP keeps the dangerous part of the payload off the request that's actually being inspected. The filter checks the request body. The dangerous instruction lives on a separate server entirely, out of view the whole time.

None of this argues for better WAF tuning, and don't read it that way. WAF rules and protocol filters sit at the perimeter and never touch how the parser itself behaves. They catch known signatures, full stop, without changing the parser's defaults. The real problem lives in the parser's defaults. The question worth asking next isn't which signature to add. That parser is running somewhere, and someone on the team may not realize it.

Where the parser is running - the hidden attack surface

Most engineers picture XXE living wherever the application calls an obvious XML API. That mental model misses most of the real attack surface, because XML parsers run in plenty of places nobody labels "XML processing."

Office Open XML files, DOCX, XLSX, PPTX, are ZIP archives holding XML internally (things like word/document.xml). Whatever library opens that archive calls an XML parser on the contents whether the developer thinks of it that way or not. A documented XLSX attack works by unzipping the file, injecting a DOCTYPE into xl/workbook.xml, rezipping it, and uploading the result. Cell-level validation, file extension allowlists, ZIP signature checks: none of it looks at the XML sitting inside the archive.

SVG is XML too, and any handler that touches an SVG upload, thumbnail generators, avatar uploads, document converters, hands the file to a parser without a second thought. Grav CMS CVE-2026-29924 is a live case: an authenticated attacker uploads through the admin panel's SVG handler, and external entity resolution sits switched on the whole time. SAML assertions are XML documents too, and XXE risks can surface in how parsers handle them. XXE findings across SAML single sign-on providers trace back to this exact pattern: attacker-controlled XML hitting a parser configured with its defaults intact.

Why parser defaults remain dangerous across ecosystems in 2026

Dangerous defaults haven't gone anywhere heading into 2026, and pinning that on any one language misses the point. Plenty of XML parsers still ship with external entity processing switched on out of the box, and bumping a library version doesn't automatically fix that. The unsafe default can survive several major versions untouched, sitting there through releases that fixed a dozen other things.

Java asks a lot of explicit work from a developer who wants safety. DocumentBuilderFactory, SAXParserFactory, and XMLInputFactory all need specific feature flags set to turn off DTD processing and external entity resolution. Skipping that step leaves the parser running unsafe by default, full stop. If a code review doesn't catch the missing flags, nothing downstream will.

Python's lxml, used everywhere, has moved its defaults more than once, and the history matters here. Older releases resolved external entities without asking. Later versions stopped fetching over the network by default, which sounds like progress, except lxml 5.x still allowed local file resolution unless a developer explicitly passed resolve_entities=False. That changed with lxml 6.1, which shifted the default to resolve_entities='internal', blocking local file access by default too. Real progress, sure, but it only protects teams actually running 6.1, not the ones with some older version pinned in a requirements file from a while back.

PHP has its own repeat offenders. simplexml_load_string() and DOMDocument->loadXML() both need LIBXML_NOENT explicitly disabled. Any SVG or document upload handler that routes through these functions without setting that flag is vulnerable, and the pattern occurs over and over across content management systems built on that same scripting language. Three languages, three different mechanisms, one underlying failure: safety requires an opt-in that most code never bothers to make.

Active exploitation in 2025-2026: CVEs that show the pattern holds

None of this is theoretical. The vulnerability record from 2025 into 2026 backs it up directly.

Apache Tika's CVE-2025-66516 rates CVSS 10.0, as critical as the scale goes. The flaw sits in how Tika processes PDF files containing XFA (XML Forms Architecture) data, and because the vulnerable code lives in tika-core, any system running versions 1.13 through 3.2.1 is exposed no matter what the surrounding application code looks like. No authentication needed. The XXE risk often lives one dependency layer down from where anyone's actually looking.

SysAid's on-premise IT service management platform carried two separate XXE vulnerabilities, CVE-2025-2775 and CVE-2025-2776, both added to CISA's Known Exploited Vulnerabilities catalog. That's confirmation of active exploitation in the field, a finding already observed in real-world attacks rather than confined to some report.

JasperReports Server's CVE-2026-16626 needs no authentication and hits versions 9.0.0 before HF-9 and 10.0.0 before HF-10, letting attackers read arbitrary files and pivot toward internal services from there. Atlassian Crowd's CVE-2026-21569, introduced in 7.1.0 and fixed in 7.1.3, carries a CVSS of 7.9: an authenticated attacker can reach local and remote content through a malicious XML payload. Different products, different vendors, same structural cause producing the same vulnerability every single time.

The emerging LLM-mediated injection path

Every case above shares a straight line: attacker sends malicious XML, application parses it. A newer variant changes the delivery mechanism without touching the underlying flaw one bit.

The parser vulnerability stays exactly the same. What shifts is who's typing the payload. Instead of an attacker submitting XML straight to a vulnerable endpoint, the attacker talks an LLM-based assistant into generating or relaying malicious XML on its own, which a downstream application then parses without knowing where any of it came from. Any SaaS product that lets an LLM produce or forward XML content, a report generator, a document assembler, an integration layer feeding another service, inherits this path the moment the parser on the receiving end still runs with its defaults untouched. The injection surface moved somewhere new. The parser stayed in place.

Sources

  1. XXE Injection: Advanced Exploitation Guide
  2. knowledge-base.secureflag.com
  3. XML External Entity Prevention - OWASP Cheat Sheet Series
  4. zsecurity.org
  5. lab.wallarm.com
  6. portswigger.net
  7. opswat.com
  8. acunetix.com

More in Exploitation Techniques