Safeguard
Vulnerability Analysis

XML External Entity (XXE) injection explained

XXE injection lets attackers abuse XML parsers to read files, hit cloud metadata via SSRF, or crash servers — here's how it works and how to stop it.

Nayan Dey
Security Researcher
7 min read

XML External Entity (XXE) injection is a vulnerability class where an application parses attacker-controlled XML without disabling external entity resolution, letting the attacker force the parser to read local files, reach internal network services, or exhaust server memory with recursive entity expansion. It's tracked as CWE-611 and was called out by name as OWASP's A4:2017-XML External Entities category before being folded into A05:2021-Security Misconfiguration. The underlying mechanism — DTD (Document Type Definition) entities — has existed since the XML 1.0 spec shipped in 1998, but the bug keeps resurfacing because libraries like Java's DocumentBuilderFactory, Python's lxml, PHP's libxml, and .NET's XmlDocument historically shipped with external entity processing turned on by default. A single unvalidated <!DOCTYPE> block in a SOAP request, SAML assertion, Office document upload, or RSS parser is enough to trigger it. This entry covers how the attack works, what attackers extract with it, and how to close it in code and in CI.

What Is an XXE Injection Vulnerability?

An XXE injection vulnerability is a flaw that lets an attacker define a custom entity inside an XML document's DTD and have the parser resolve it against a resource the attacker chooses instead of the literal text the application expects. XML parsers support two entity types relevant here: internal entities (plain text substitutions) and external entities, which can point to a file://, http://, or ftp:// URI. If an application's parser has external entity resolution enabled — the default in most XML libraries before roughly 2015–2017 — any XML field that reaches that parser becomes a path to the filesystem or internal network the application runs on. This is distinct from XML injection or XPath injection; XXE specifically abuses the DTD/entity subsystem, which is why the fix is almost always "disable DTDs entirely" rather than "sanitize input."

How Does an XXE Attack Actually Work?

An XXE attack works by smuggling a DOCTYPE declaration with a SYSTEM or PUBLIC entity into XML that the target application parses, then referencing that entity in the document body so the parser substitutes its resolved value into the response. The canonical local-file-disclosure payload looks like this:

<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>

If the application echoes parsed XML content back — a common pattern in SOAP APIs, PDF/Office metadata extractors, and RSS/Atom feed readers — the contents of /etc/passwd come back in the response. When the application doesn't echo anything (a "blind" XXE), attackers use out-of-band techniques: an external DTD hosted on an attacker-controlled server with a parameter entity that exfiltrates data via a follow-up HTTP request, first documented widely by security researcher Timothy Morgan's 2014 "XML Schema, DTD, and Entity Attacks" paper.

What Can an Attacker Achieve With an XXE Vulnerability?

An attacker exploiting XXE can read arbitrary local files, pivot into internal-only network services, or crash the server, depending on which entity type they use. The four concrete impact categories are:

  • Local file disclosure — reading /etc/passwd, application config files, private keys, or .env files via file:// URIs.
  • Server-Side Request Forgery (SSRF) — using SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/" to pull cloud IAM credentials from the AWS/GCP/Azure instance metadata service, one of the most common real-world XXE-to-cloud-compromise chains.
  • Denial of service — the "billion laughs" attack, where nested entities each reference the previous one ten times over ten levels, expand a ~1KB payload into roughly a gigabyte of memory before the parser finishes, first named in a 2003 mailing list post and still functional against unpatched parsers today.
  • Port scanning and protocol abuse — using external entities to probe internal hosts and ports, inferring what's open from response timing or error differences.

Which Real-World Incidents Were Caused by XXE?

Real-world XXE incidents include a $33,500 Facebook bug bounty, a critical Apache Solr CVE, and a Django XML-RPC flaw, all stemming from the same root cause: parsers left in their insecure default configuration. In 2014, researcher Reginaldo Silva reported an XXE in Facebook's "Notes" import feature that could reach internal hosts via blind SSRF; Facebook paid one of its largest bounties at the time — $33,500 — and Silva published the full technical writeup. CVE-2017-12629 affected Apache Solr's XML query parser, allowing unauthenticated XXE that chained into remote code execution via Solr's RunExecutableListener. CVE-2013-0155 was an XXE in Django's XML-RPC handling that let attackers read arbitrary files on the host filesystem through the dispatch() method's XML parsing path. Each case traces back to the same one-line fix: the parser needed DTD processing disabled and never got it.

How Do You Detect and Prevent XXE in Your Codebase?

You prevent XXE by disabling DTD and external entity processing at the parser level, not by trying to filter malicious-looking XML at the input layer. Concretely:

  • Java: set DocumentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) and disable FEATURE_SECURE_PROCESSING external access — this is the fix OWASP's Java cheat sheet has recommended since at least 2014.
  • Python (lxml): pass resolve_entities=False and no_network=True to etree.XMLParser(); xml.etree.ElementTree was patched to resist billion-laughs in Python 3.7.1 (2018) via defusedxml-style guards, but external entities still require explicit configuration.
  • .NET: use XmlReaderSettings with DtdProcessing = DtdProcessing.Prohibit — the .NET Framework 4.5.2+ default for XmlTextReader changed to disallow DTDs by default in 2015 (MS14-XXX servicing update era), but older codebases and third-party libraries frequently override it.
  • PHP: call libxml_disable_entity_loader(true) before parsing (required behavior pre-PHP 8.0; PHP 8.0, released November 2020, disabled external entity loading by default).

For detection, static analysis rules that flag DocumentBuilderFactory, SAXParserFactory, XMLInputFactory, and lxml.etree instantiation without the corresponding hardening call are far more reliable than regex-based input scanning, because the vulnerability lives in parser configuration, not in any single tainted string.

Is XXE Still a Risk in Software Supply Chains in 2026?

Yes — XXE remains a live risk in 2026 primarily through transitive dependencies and legacy SOAP/SAML integrations rather than newly written application code. Modern frameworks (Spring Boot 2.x+, Django 3+, most Node XML libraries) ship safe defaults now, but organizations still run XML parsing through older third-party JARs, vendored SDKs, or SAML identity-provider libraries where the insecure default was never changed. A dependency scanner that only checks "is this package version vulnerable" misses the more common 2026-era case: a current, non-vulnerable XML library that the calling code has explicitly reconfigured to allow external entities for a legitimate-looking reason (custom DTD support for a partner integration, for example) years ago and never revisited.

How Safeguard Helps

Safeguard finds XXE exposure the way it actually shows up in production: as a reachable code path, not just a flagged package version. Our reachability analysis traces whether attacker-controlled input — an HTTP body, an uploaded file, a webhook payload — actually flows into an XML parser instantiation before that parser's DTD and external entity settings are locked down, so teams triage the handful of calls that matter instead of every XML dependency in the SBOM. Griffin AI reads the surrounding code to confirm whether entity resolution is genuinely disabled at that call site and explains the specific data flow in plain language during review. Safeguard's SBOM generation and ingest pipeline inventories every XML-parsing library across your services — including transitive SOAP/SAML dependencies — so nothing gets missed in an audit, and our auto-fix PRs apply the correct hardening call (disallow-doctype-decl, resolve_entities=False, DtdProcessing.Prohibit, and equivalents) directly to the vulnerable parser configuration for engineering to review and merge.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.